Repository: spencerahill/aospy

Issue:
New failure in test_apply_time_offset
After reverting to xarray 0.11, c.f. #319, the test suite loads without error but has one failure, which I've pasted below.  @spencerkclark, since this seems cftime-related (or adjacent), I'm going to punt this one to you.

```python
test_apply_time_offset ___________________________________________________________________

cls = <class 'pandas.core.arrays.datetimes.DatetimeArray'>
index = <DatetimeArray>
['1898-06-30 00:00:00', '1898-07-30 00:00:00']
Length: 2, dtype: datetime64[ns], freq = <MonthEnd>, kwargs = {}, inferred = None
on_freq = <DatetimeArray>
['1898-06-30 00:00:00', '1898-07-31 00:00:00']
Length: 2, dtype: datetime64[ns]

    @classmethod
    def _validate_frequency(cls, index, freq, **kwargs):
        """
        Validate that a frequency is compatible with the values of a given
        Datetime Array/Index or Timedelta Array/Index

        Parameters
        ----------
        index : DatetimeIndex or TimedeltaIndex
            The index on which to determine if the given frequency is valid
        freq : DateOffset
            The frequency to validate
        """
        if is_period_dtype(cls):
            # Frequency validation is not meaningful for Period Array/Index
            return None

        inferred = index.inferred_freq
        if index.size == 0 or inferred == freq.freqstr:
            return None

        try:
            on_freq = cls._generate_range(start=index[0], end=None,
                                          periods=len(index), freq=freq,
                                          **kwargs)
            if not np.array_equal(index.asi8, on_freq.asi8):
>               raise ValueError
E               ValueError

../../pandas/core/arrays/datetimelike.py:884: ValueError

During handling of the above exception, another exception occurred:

    def test_apply_time_offset():
        start = datetime.datetime(1900, 5, 10)
        years, months, days, hours = -2, 1, 7, 3
        # test lengths 0, 1, and >1 of input time array
        for periods in range(3):
            times = pd.date_range(start=start, freq='M', periods=periods)
            actual = apply_time_offset(xr.DataArray(times), years=years,
                                       months=months, days=days, hours=hours)
            desired = (times + pd.tseries.offsets.DateOffset(
>               years=years, months=months, days=days, hours=hours
            ))

test_utils_times.py:55:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../pandas/core/indexes/datetimelike.py:489: in __add__
    result = self._data.__add__(maybe_unwrap_index(other))
../../pandas/core/arrays/datetimelike.py:1190: in __add__
    result = self._add_offset(other)
../../pandas/core/arrays/datetimes.py:737: in _add_offset
    result = offset.apply_index(values)
pandas/_libs/tslibs/offsets.pyx:116: in pandas._libs.tslibs.offsets.apply_index_wraps.wrapper
    ???
../../pandas/tseries/offsets.py:278: in apply_index
    i = type(i)(shifted, freq=i.freq, dtype=i.dtype)
../../pandas/core/arrays/datetimes.py:351: in __init__
    type(self)._validate_frequency(self, freq)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class 'pandas.core.arrays.datetimes.DatetimeArray'>
index = <DatetimeArray>
['1898-06-30 00:00:00', '1898-07-30 00:00:00']
Length: 2, dtype: datetime64[ns], freq = <MonthEnd>, kwargs = {}, inferred = None
on_freq = <DatetimeArray>
['1898-06-30 00:00:00', '1898-07-31 00:00:00']
Length: 2, dtype: datetime64[ns]

    @classmethod
    def _validate_frequency(cls, index, freq, **kwargs):
        """
        Validate that a frequency is compatible with the values of a given
        Datetime Array/Index or Timedelta Array/Index

        Parameters
        ----------
        index : DatetimeIndex or TimedeltaIndex
            The index on which to determine if the given frequency is valid
        freq : DateOffset
            The frequency to validate
        """
        if is_period_dtype(cls):
            # Frequency validation is not meaningful for Period Array/Index
            return None

        inferred = index.inferred_freq
        if index.size == 0 or inferred == freq.freqstr:
            return None

        try:
            on_freq = cls._generate_range(start=index[0], end=None,
                                          periods=len(index), freq=freq,
                                          **kwargs)
            if not np.array_equal(index.asi8, on_freq.asi8):
                raise ValueError
        except ValueError as e:
            if "non-fixed" in str(e):
                # non-fixed frequencies are not meaningful for timedelta64;
                #  we retain that error message
                raise e
            # GH#11587 the main way this is reached is if the `np.array_equal`
            #  check above is False.  This can also be reached if index[0]
            #  is `NaT`, in which case the call to `cls._generate_range` will
            #  raise a ValueError, which we re-raise with a more targeted
            #  message.
            raise ValueError('Inferred frequency {infer} from passed values '
                             'does not conform to passed frequency {passed}'
>                            .format(infer=inferred, passed=freq.freqstr))
E           ValueError: Inferred frequency None from passed values does not conform to passed frequency M

../../pandas/core/arrays/datetimelike.py:897: ValueError
```

Discussion:
spencerahill: LGTM.  Feel free to merge.  Thanks!

Patch:
diff --git a/aospy/utils/times.py b/aospy/utils/times.py
index beaea13..702e3ec 100644
--- a/aospy/utils/times.py
+++ b/aospy/utils/times.py
@@ -55,8 +55,7 @@ def apply_time_offset(time, years=0, months=0, days=0, hours=0):
                   freq=None)
     """
     return (pd.to_datetime(time.values) +
-            pd.tseries.offsets.DateOffset(years=years, months=months,
-                                          days=days, hours=hours))
+            pd.DateOffset(years=years, months=months, days=days, hours=hours))
 
 
 def average_time_bounds(ds):
@@ -512,12 +511,15 @@ def ensure_time_as_index(ds):
     time_indexed_coords = {TIME_WEIGHTS_STR, TIME_BOUNDS_STR}
     time_indexed_vars = set(ds.data_vars).union(time_indexed_coords)
     time_indexed_vars = time_indexed_vars.intersection(ds.variables)
+    variables_to_replace = {}
     for name in time_indexed_vars:
         if TIME_STR not in ds[name].indexes:
-            da = ds[name].expand_dims(TIME_STR)
-            da[TIME_STR] = ds[TIME_STR]
-            ds[name] = da
-    return ds
+            da = ds[name]
+            if TIME_STR not in da.dims:
+                da = ds[name].expand_dims(TIME_STR)
+            da = da.assign_coords(**{TIME_STR: ds[TIME_STR]})
+            variables_to_replace[name] = da
+    return ds.assign(**variables_to_replace)
 
 
 def infer_year(date):


Test patch:
diff --git a/aospy/test/test_utils_times.py b/aospy/test/test_utils_times.py
index ce94760..3fe4bd4 100755
--- a/aospy/test/test_utils_times.py
+++ b/aospy/test/test_utils_times.py
@@ -49,9 +49,10 @@ def test_apply_time_offset():
     # test lengths 0, 1, and >1 of input time array
     for periods in range(3):
         times = pd.date_range(start=start, freq='M', periods=periods)
+        times = pd.to_datetime(times.values)  # Workaround for pandas bug
         actual = apply_time_offset(xr.DataArray(times), years=years,
                                    months=months, days=days, hours=hours)
-        desired = (times + pd.tseries.offsets.DateOffset(
+        desired = (times + pd.DateOffset(
             years=years, months=months, days=days, hours=hours
         ))
         assert actual.identical(desired)
@@ -455,6 +456,7 @@ def test_ensure_time_as_index_with_change():
         coords={TIME_STR: arr[TIME_STR]}
     )
     ds = ds.isel(**{TIME_STR: 0}).expand_dims(TIME_STR)
+    initial_ds = ds.copy()
     actual = ensure_time_as_index(ds)
     expected = arr.to_dataset(name='a')
     expected.coords[TIME_WEIGHTS_STR] = xr.DataArray(
@@ -465,6 +467,9 @@ def test_ensure_time_as_index_with_change():
         coords={TIME_STR: arr[TIME_STR]}
         )
     xr.testing.assert_identical(actual, expected)
+    # Make sure input Dataset was not mutated by the call
+    # to ensure_time_as_index
+    xr.testing.assert_identical(ds, initial_ds)
 
 
 def test_sel_time():