From 0bd61d90a0994cbcffed296d28234743be6229bb Mon Sep 17 00:00:00 2001 From: AS Date: Mon, 25 May 2020 17:47:04 +0200 Subject: [PATCH 01/35] add property --- xarray/coding/cftimeindex.py | 6 ++++++ xarray/tests/test_cftimeindex.py | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 6fc28d213dd..79d702b210d 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -581,6 +581,12 @@ def asi8(self): ] ) + @property + def calendar(self): + from .times import infer_calendar_name + + return infer_calendar_name(self) + def _round_via_method(self, freq, method): """Round dates using a specified method.""" from .cftime_offsets import CFTIME_TICKS, to_offset diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index b30e32c92ad..3e32ce95d24 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -884,6 +884,18 @@ def test_cftimeindex_shift_invalid_freq(): index.shift(1, 1) +@requires_cftime +@pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) +def test_cftimeindex_calendar_property(calendar): + index = xr.cftime_range(start="2000", periods=3, calendar=calendar) + # is this awkward here in a test? + if calendar == "365_day": + calendar = "noleap" + elif calendar == "366_day": + calendar = "all_leap" + assert index.calendar == calendar + + @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap From 28cbf84b8da2e5f262f6c95b66dfd91d6ae00ce7 Mon Sep 17 00:00:00 2001 From: AS Date: Mon, 25 May 2020 17:57:44 +0200 Subject: [PATCH 02/35] test repr skip --- xarray/tests/test_cftimeindex.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 3e32ce95d24..a3f41abd926 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -896,6 +896,21 @@ def test_cftimeindex_calendar_property(calendar): assert index.calendar == calendar +@requires_cftime +@pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) +@pytest.mark.skip(reason="not implemented") +def test_cftimeindex_calendar_in_repr(calendar): + index = xr.cftime_range(start="2000", periods=3, calendar=calendar) + # is this awkward here in a test? + repr_str = index.__repr__() + if calendar == "365_day": + calendar = "noleap" + elif calendar == "366_day": + calendar = "all_leap" + assert calendar in repr_str + assert "calendar=" in repr_str + + @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap From c5d36ccfe96d01c0cc3844bc2917b89aed4b036f Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 3 Jun 2020 21:37:46 +0200 Subject: [PATCH 03/35] repr --- doc/whats-new.rst | 2 ++ xarray/coding/cftimeindex.py | 53 ++++++++++++++++++++++++++++++++ xarray/tests/test_cftimeindex.py | 44 +++++++++++++++----------- 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index a4602c1edad..70ad67b0ecf 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -82,6 +82,8 @@ New Features :py:func:`xarray.decode_cf`) that allows to disable/enable the decoding of timedeltas independently of time decoding (:issue:`1621`) `Aureliana Barghini ` +- Add ``calendar`` as a new property for ``cftimeindex`` and show in ``__repr__`` (:issue:`2416`, :pull:`4092`) +`Aaron Spring ` Bug fixes ~~~~~~~~~ diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 79d702b210d..361f5caea5d 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -46,6 +46,8 @@ import numpy as np import pandas as pd +from pandas.core.dtypes.common import is_object_dtype +from pandas.io.formats.printing import format_object_attrs, format_object_summary from xarray.core.utils import is_scalar @@ -259,6 +261,57 @@ def __new__(cls, data, name=None): result._cache = {} return result + def __repr__(self): + """ + copied from pandas.io.printing.py expect for attrs.append(("calendar", self.calendar)) + Return a string representation for this object. + """ + klass_name = type(self).__name__ + data = self._format_data() + attrs = self._format_attrs() + # add calendar to attrs + attrs.append(("calendar", self.calendar)) + space = self._format_space() + attrs_str = [f"{k}={v}" for k, v in attrs] + prepr = f",{space}".join(attrs_str) + + # no data provided, just attributes + if data is None: + data = "" + + res = f"{klass_name}({data}{prepr})" + + return res + + def _format_space(self): + """copied from pandas.io.printing.py""" + return " " + + def _format_data(self, name=None): + """ + copied from pandas.io.printing.py + Return the formatted data as a unicode string. + """ + # do we want to justify (only do so for non-objects) + is_justify = True + + if self.inferred_type == "string": + is_justify = False + elif self.inferred_type == "categorical": + if is_object_dtype(self.categories): # type: ignore + is_justify = False + + return format_object_summary( + self, self._formatter_func, is_justify=is_justify, name=name + ) + + def _format_attrs(self): + """ + copied from pandas.io.printing.py + Return a list of tuples of the (attr,formatted_value). + """ + return format_object_attrs(self) + def _partial_date_slice(self, resolution, parsed): """Adapted from pandas.tseries.index.DatetimeIndex._partial_date_slice diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index a3f41abd926..0e0e286c6db 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -885,30 +885,38 @@ def test_cftimeindex_shift_invalid_freq(): @requires_cftime -@pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) -def test_cftimeindex_calendar_property(calendar): +@pytest.mark.parametrize( + ("calendar", "expected"), + [ + ("noleap", "noleap"), + ("365_day", "noleap"), + ("360_day", "360_day"), + ("julian", "julian"), + ("gregorian", "gregorian"), + ("proleptic_gregorian", "proleptic_gregorian"), + ], +) +def test_cftimeindex_calendar_property(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) - # is this awkward here in a test? - if calendar == "365_day": - calendar = "noleap" - elif calendar == "366_day": - calendar = "all_leap" - assert index.calendar == calendar + assert index.calendar == expected @requires_cftime -@pytest.mark.parametrize("calendar", _CFTIME_CALENDARS) -@pytest.mark.skip(reason="not implemented") -def test_cftimeindex_calendar_in_repr(calendar): +@pytest.mark.parametrize( + ("calendar", "expected"), + [ + ("noleap", "noleap"), + ("365_day", "noleap"), + ("360_day", "360_day"), + ("julian", "julian"), + ("gregorian", "gregorian"), + ("proleptic_gregorian", "proleptic_gregorian"), + ], +) +def test_cftimeindex_calendar_in_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) - # is this awkward here in a test? repr_str = index.__repr__() - if calendar == "365_day": - calendar = "noleap" - elif calendar == "366_day": - calendar = "all_leap" - assert calendar in repr_str - assert "calendar=" in repr_str + assert f" calendar={expected}" in repr_str @requires_cftime From 8d9ebe0294d1991d995d96412b70515c11bcb4c6 Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 3 Jun 2020 22:47:01 +0200 Subject: [PATCH 04/35] linting --- doc/whats-new.rst | 2 +- xarray/coding/cftimeindex.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 70ad67b0ecf..9979628ea88 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -83,7 +83,7 @@ New Features independently of time decoding (:issue:`1621`) `Aureliana Barghini ` - Add ``calendar`` as a new property for ``cftimeindex`` and show in ``__repr__`` (:issue:`2416`, :pull:`4092`) -`Aaron Spring ` + `Aaron Spring ` Bug fixes ~~~~~~~~~ diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 361f5caea5d..fa55e1ddfa0 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -263,8 +263,10 @@ def __new__(cls, data, name=None): def __repr__(self): """ - copied from pandas.io.printing.py expect for attrs.append(("calendar", self.calendar)) Return a string representation for this object. + + copied from pandas.io.printing.py + expect for attrs.append(("calendar", self.calendar)) """ klass_name = type(self).__name__ data = self._format_data() @@ -289,8 +291,9 @@ def _format_space(self): def _format_data(self, name=None): """ - copied from pandas.io.printing.py Return the formatted data as a unicode string. + + copied from pandas.io.printing.py """ # do we want to justify (only do so for non-objects) is_justify = True @@ -307,8 +310,9 @@ def _format_data(self, name=None): def _format_attrs(self): """ - copied from pandas.io.printing.py Return a list of tuples of the (attr,formatted_value). + + copied from pandas.io.printing.py """ return format_object_attrs(self) From c1cac9f262885a4311b071b464c8026ba0541824 Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 3 Jun 2020 23:28:03 +0200 Subject: [PATCH 05/35] remove unnecessary --- xarray/coding/cftimeindex.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index fa55e1ddfa0..4510711997c 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -285,37 +285,6 @@ def __repr__(self): return res - def _format_space(self): - """copied from pandas.io.printing.py""" - return " " - - def _format_data(self, name=None): - """ - Return the formatted data as a unicode string. - - copied from pandas.io.printing.py - """ - # do we want to justify (only do so for non-objects) - is_justify = True - - if self.inferred_type == "string": - is_justify = False - elif self.inferred_type == "categorical": - if is_object_dtype(self.categories): # type: ignore - is_justify = False - - return format_object_summary( - self, self._formatter_func, is_justify=is_justify, name=name - ) - - def _format_attrs(self): - """ - Return a list of tuples of the (attr,formatted_value). - - copied from pandas.io.printing.py - """ - return format_object_attrs(self) - def _partial_date_slice(self, resolution, parsed): """Adapted from pandas.tseries.index.DatetimeIndex._partial_date_slice From d09092a8abd94bbf55530f1490bde05dfa1f1c47 Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 3 Jun 2020 23:28:57 +0200 Subject: [PATCH 06/35] remove unnecessary --- xarray/coding/cftimeindex.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 4510711997c..2524c1df6dc 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -46,8 +46,6 @@ import numpy as np import pandas as pd -from pandas.core.dtypes.common import is_object_dtype -from pandas.io.formats.printing import format_object_attrs, format_object_summary from xarray.core.utils import is_scalar From 59dc9122c7ed0fed8af5d66beaab5bef60ca8a0f Mon Sep 17 00:00:00 2001 From: AS Date: Thu, 4 Jun 2020 09:25:06 +0200 Subject: [PATCH 07/35] add quotation marks to calendar --- xarray/coding/cftimeindex.py | 2 +- xarray/tests/test_cftimeindex.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 2524c1df6dc..8254b2ff591 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -270,7 +270,7 @@ def __repr__(self): data = self._format_data() attrs = self._format_attrs() # add calendar to attrs - attrs.append(("calendar", self.calendar)) + attrs.append(("calendar", f"'{self.calendar}'")) space = self._format_space() attrs_str = [f"{k}={v}" for k, v in attrs] prepr = f",{space}".join(attrs_str) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 0e0e286c6db..556e6e54cc2 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -916,7 +916,7 @@ def test_cftimeindex_calendar_property(calendar, expected): def test_cftimeindex_calendar_in_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() - assert f" calendar={expected}" in repr_str + assert f" calendar='{expected}'" in repr_str @requires_cftime From 1fccb53605bae0122d1ad4bb65dd78201c935342 Mon Sep 17 00:00:00 2001 From: AS Date: Thu, 4 Jun 2020 09:29:01 +0200 Subject: [PATCH 08/35] add length to wrapper --- doc/whats-new.rst | 3 ++- xarray/coding/cftimeindex.py | 2 ++ xarray/tests/test_cftimeindex.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 9979628ea88..b496b709aaa 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -82,7 +82,8 @@ New Features :py:func:`xarray.decode_cf`) that allows to disable/enable the decoding of timedeltas independently of time decoding (:issue:`1621`) `Aureliana Barghini ` -- Add ``calendar`` as a new property for ``cftimeindex`` and show in ``__repr__`` (:issue:`2416`, :pull:`4092`) +- Add ``calendar`` as a new property for ``cftimeindex`` and show in ``calendar`` and + ``length`` in ``__repr__`` (:issue:`2416`, :pull:`4092`) `Aaron Spring ` Bug fixes diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 8254b2ff591..d611de7d278 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -271,6 +271,8 @@ def __repr__(self): attrs = self._format_attrs() # add calendar to attrs attrs.append(("calendar", f"'{self.calendar}'")) + # add length to attrs + attrs.append(("length", f"{len(self)}")) space = self._format_space() attrs_str = [f"{k}={v}" for k, v in attrs] prepr = f",{space}".join(attrs_str) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 556e6e54cc2..ab631edf5ad 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -917,6 +917,7 @@ def test_cftimeindex_calendar_in_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str + assert f" length=3" @requires_cftime From a52e997d7a03233b243ecbd29617763627acdc8c Mon Sep 17 00:00:00 2001 From: AS Date: Thu, 4 Jun 2020 09:30:06 +0200 Subject: [PATCH 09/35] linting --- xarray/tests/test_cftimeindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index ab631edf5ad..39efbe5a509 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -917,7 +917,7 @@ def test_cftimeindex_calendar_in_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str - assert f" length=3" + assert " length=3" @requires_cftime From 6d022ea89cdb74f0d84a2d35e36de126ef5e46be Mon Sep 17 00:00:00 2001 From: AS Date: Sat, 6 Jun 2020 19:27:13 +0200 Subject: [PATCH 10/35] coords.to_index() if CFTimeIndex --- xarray/coding/cftimeindex.py | 44 +++++++++++------ xarray/core/formatting.py | 55 ++++++++++++++++----- xarray/core/formatting_html.py | 13 +++-- xarray/tests/test_cftimeindex.py | 85 +++++++++++++++++++++++--------- 4 files changed, 144 insertions(+), 53 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index d611de7d278..80842dbc293 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -182,7 +182,8 @@ def f(self, min_cftime_version=min_cftime_version): raise ImportError( "The {!r} accessor requires a minimum " "version of cftime of {}. Found an " - "installed version of {}.".format(name, min_cftime_version, version) + "installed version of {}.".format( + name, min_cftime_version, version) ) f.__name__ = name @@ -238,11 +239,13 @@ class CFTimeIndex(pd.Index): hour = _field_accessor("hour", "The hours of the datetime") minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") - microsecond = _field_accessor("microsecond", "The microseconds of the datetime") + microsecond = _field_accessor( + "microsecond", "The microseconds of the datetime") dayofyear = _field_accessor( "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" ) - dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") + dayofweek = _field_accessor( + "dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" ) @@ -259,6 +262,10 @@ def __new__(cls, data, name=None): result._cache = {} return result + @property + def data(self): + return self.values + def __repr__(self): """ Return a string representation for this object. @@ -269,10 +276,10 @@ def __repr__(self): klass_name = type(self).__name__ data = self._format_data() attrs = self._format_attrs() - # add calendar to attrs - attrs.append(("calendar", f"'{self.calendar}'")) # add length to attrs attrs.append(("length", f"{len(self)}")) + # add calendar to attrs + attrs.append(("calendar", f"'{self.calendar}'")) space = self._format_space() attrs_str = [f"{k}={v}" for k, v in attrs] prepr = f",{space}".join(attrs_str) @@ -330,7 +337,8 @@ def _partial_date_slice(self, resolution, parsed): Coordinates: * time (time) datetime64[ns] 2001-01-01T01:00:00 """ - start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) + start, end = _parsed_string_to_bounds( + self.date_type, resolution, parsed) times = self._data @@ -368,13 +376,16 @@ def _get_nearest_indexer(self, target, limit, tolerance): right_distances = abs(self.values[right_indexer] - target.values) if self.is_monotonic_increasing: - condition = (left_distances < right_distances) | (right_indexer == -1) + condition = (left_distances < right_distances) | ( + right_indexer == -1) else: - condition = (left_distances <= right_distances) | (right_indexer == -1) + condition = (left_distances <= right_distances) | ( + right_indexer == -1) indexer = np.where(condition, left_indexer, right_indexer) if tolerance is not None: - indexer = self._filter_indexer_tolerance(target, indexer, tolerance) + indexer = self._filter_indexer_tolerance( + target, indexer, tolerance) return indexer def _filter_indexer_tolerance(self, target, indexer, tolerance): @@ -397,8 +408,10 @@ def _maybe_cast_slice_bound(self, label, side, kind): """Adapted from pandas.tseries.index.DatetimeIndex._maybe_cast_slice_bound""" if isinstance(label, str): - parsed, resolution = _parse_iso8601_with_reso(self.date_type, label) - start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) + parsed, resolution = _parse_iso8601_with_reso( + self.date_type, label) + start, end = _parsed_string_to_bounds( + self.date_type, resolution, parsed) if self.is_monotonic_decreasing and len(self) > 1: return end if side == "left" else start return start if side == "left" else end @@ -602,7 +615,8 @@ def asi8(self): epoch = self.date_type(1970, 1, 1) return np.array( [ - _total_microseconds(exact_cftime_datetime_difference(epoch, date)) + _total_microseconds( + exact_cftime_datetime_difference(epoch, date)) for date in self.values ] ) @@ -728,7 +742,8 @@ def _cftimeindex_from_i8(values, date_type, name): CFTimeIndex """ epoch = date_type(1970, 1, 1) - dates = np.array([epoch + timedelta(microseconds=int(value)) for value in values]) + dates = np.array([epoch + timedelta(microseconds=int(value)) + for value in values]) return CFTimeIndex(dates, name=name) @@ -763,7 +778,8 @@ def _round_to_nearest_half_even(values, unit): return _ceil_int(values - unit // 2, unit) quotient, remainder = np.divmod(values, unit) mask = np.logical_or( - remainder > (unit // 2), np.logical_and(remainder == (unit // 2), quotient % 2) + remainder > (unit // 2), np.logical_and(remainder == + (unit // 2), quotient % 2) ) quotient[mask] += 1 return quotient * unit diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index d6732fc182e..289f2634fc6 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -65,7 +65,8 @@ def first_n_items(array, n_desired): return [] if n_desired < array.size: - indexer = _get_indexer_at_least_n_items(array.shape, n_desired, from_end=False) + indexer = _get_indexer_at_least_n_items( + array.shape, n_desired, from_end=False) array = array[indexer] return np.asarray(array).flat[:n_desired] @@ -80,7 +81,8 @@ def last_n_items(array, n_desired): return [] if n_desired < array.size: - indexer = _get_indexer_at_least_n_items(array.shape, n_desired, from_end=True) + indexer = _get_indexer_at_least_n_items( + array.shape, n_desired, from_end=True) array = array[indexer] return np.asarray(array).flat[-n_desired:] @@ -152,7 +154,8 @@ def format_items(x): timedelta_format = "datetime" if np.issubdtype(x.dtype, np.timedelta64): x = np.asarray(x, dtype="timedelta64[ns]") - day_part = x[~pd.isnull(x)].astype("timedelta64[D]").astype("timedelta64[ns]") + day_part = x[~pd.isnull(x)].astype( + "timedelta64[D]").astype("timedelta64[ns]") time_needed = x[~pd.isnull(x)] != day_part day_needed = day_part != np.timedelta64(0, "ns") if np.logical_not(day_needed).all(): @@ -176,7 +179,8 @@ def format_array_flat(array, max_width: int): relevant_front_items = format_items( first_n_items(array, (max_possibly_relevant + 1) // 2) ) - relevant_back_items = format_items(last_n_items(array, max_possibly_relevant // 2)) + relevant_back_items = format_items( + last_n_items(array, max_possibly_relevant // 2)) # interleave relevant front and back items: # [a, b, c] and [y, z] -> [a, z, b, y, c] relevant_items = sum( @@ -189,7 +193,8 @@ def format_array_flat(array, max_width: int): ): padding = " ... " count = min( - array.size, max(np.argmax(cum_len + len(padding) - 1 > max_width), 2) + array.size, max( + np.argmax(cum_len + len(padding) - 1 > max_width), 2) ) else: count = array.size @@ -275,7 +280,8 @@ def summarize_variable( if max_width is None: max_width_options = OPTIONS["display_width"] if not isinstance(max_width_options, int): - raise TypeError(f"`max_width` value of `{max_width}` is not a valid int") + raise TypeError( + f"`max_width` value of `{max_width}` is not a valid int") else: max_width = max_width_options first_col = pretty_print(f" {marker} {name} ", col_width) @@ -427,7 +433,8 @@ def short_numpy_repr(array): # default to lower precision so a full (abbreviated) line can fit on # one line with the default display_width - options = {"precision": 6, "linewidth": OPTIONS["display_width"], "threshold": 200} + options = {"precision": 6, + "linewidth": OPTIONS["display_width"], "threshold": 200} if array.ndim < 3: edgeitems = 3 elif array.ndim == 3: @@ -441,9 +448,28 @@ def short_numpy_repr(array): def short_data_repr(array): """Format "data" for DataArray and Variable.""" + from ..coding.cftimeindex import CFTimeIndex + # try to convert to index: needed to get CFTimeIndex in html repr + try: + array2 = array.to_index() + # doesnt catch I dont understand why + assert isinstance(array2, CFTimeIndex) + if not isinstance(array2, CFTimeIndex): + print('revert') + array2 = array + except ValueError: # catch to_index() fails + array2 = array + except AssertionError: # catch other types than CFTimeIndex + array2 = array + except: + array2 = array + array = array2 + internal_data = getattr(array, "variable", array)._data if isinstance(array, np.ndarray): return short_numpy_repr(array) + elif isinstance(array, CFTimeIndex): + return repr(array) elif hasattr(internal_data, "__array_function__") or isinstance( internal_data, dask_array_type ): @@ -463,7 +489,8 @@ def array_repr(arr): name_str = "" summary = [ - "".format(type(arr).__name__, name_str, dim_summary(arr)), + "".format( + type(arr).__name__, name_str, dim_summary(arr)), short_data_repr(arr), ] @@ -569,7 +596,8 @@ def extra_items_repr(extra_keys, mapping, ab_side): for var_s, attr_s in zip(temp, attrs_summary) ] - diff_items += [ab_side + s[1:] for ab_side, s in zip(("L", "R"), temp)] + diff_items += [ab_side + s[1:] + for ab_side, s in zip(("L", "R"), temp)] if diff_items: summary += ["Differing {}:".format(title.lower())] + diff_items @@ -613,7 +641,8 @@ def diff_array_repr(a, b, compat): summary.append(diff_dim_summary(a, b)) if not array_equiv(a.data, b.data): - temp = [wrap_indent(short_numpy_repr(obj), start=" ") for obj in (a, b)] + temp = [wrap_indent(short_numpy_repr(obj), start=" ") + for obj in (a, b)] diff_data_repr = [ ab_side + "\n" + ab_data_repr for ab_side, ab_data_repr in zip(("L", "R"), temp) @@ -644,9 +673,11 @@ def diff_dataset_repr(a, b, compat): ) summary.append(diff_dim_summary(a, b)) - summary.append(diff_coords_repr(a.coords, b.coords, compat, col_width=col_width)) + summary.append(diff_coords_repr( + a.coords, b.coords, compat, col_width=col_width)) summary.append( - diff_data_vars_repr(a.data_vars, b.data_vars, compat, col_width=col_width) + diff_data_vars_repr(a.data_vars, b.data_vars, + compat, col_width=col_width) ) if compat == "identical": diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 69832d6ca3d..a692aeb9ca0 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -8,11 +8,13 @@ from .formatting import inline_variable_array_repr, short_data_repr CSS_FILE_PATH = "/".join(("static", "css", "style.css")) -CSS_STYLE = pkg_resources.resource_string("xarray", CSS_FILE_PATH).decode("utf8") +CSS_STYLE = pkg_resources.resource_string( + "xarray", CSS_FILE_PATH).decode("utf8") ICONS_SVG_PATH = "/".join(("static", "html", "icons-svg-inline.html")) -ICONS_SVG = pkg_resources.resource_string("xarray", ICONS_SVG_PATH).decode("utf8") +ICONS_SVG = pkg_resources.resource_string( + "xarray", ICONS_SVG_PATH).decode("utf8") def short_data_repr_html(array): @@ -85,8 +87,8 @@ def summarize_coords(variables): for k, v in variables.items(): coords.update(**summarize_coord(k, v)) - vars_li = "".join(f"
  • {v}
  • " for v in coords.values()) - + vars_li = "".join( + f"
  • {v}
  • " for v in coords.values()) return f"
      {vars_li}
    " @@ -229,7 +231,8 @@ def _obj_repr(obj, header_components, sections): """ header = f"
    {''.join(h for h in header_components)}
    " - sections = "".join(f"
  • {s}
  • " for s in sections) + sections = "".join( + f"
  • {s}
  • " for s in sections) return ( "
    " diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 39efbe5a509..9c0e8620ab3 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -194,7 +194,8 @@ def test_assert_all_valid_date_type(date_type, index): with pytest.raises(TypeError): assert_all_valid_date_type(np.array([1, date_type(1, 1, 1)])) - assert_all_valid_date_type(np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) + assert_all_valid_date_type( + np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) @requires_cftime @@ -261,7 +262,8 @@ def test_parse_string_to_bounds_year(date_type, dec_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 1, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "year", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "year", parsed) assert result_start == expected_start assert result_end == expected_end @@ -271,7 +273,8 @@ def test_parse_string_to_bounds_month_feb(date_type, feb_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 2, 1) expected_end = date_type(2, 2, feb_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -281,7 +284,8 @@ def test_parse_string_to_bounds_month_dec(date_type, dec_days): parsed = date_type(2, 12, 1) expected_start = date_type(2, 12, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -303,7 +307,8 @@ def test_parsed_string_to_bounds_sub_monthly( expected_start = date_type(*ex_start_args) expected_end = date_type(*ex_end_args) - result_start, result_end = _parsed_string_to_bounds(date_type, reso, parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, reso, parsed) assert result_start == expected_start assert result_end == expected_end @@ -388,7 +393,8 @@ def test_get_slice_bound_length_one_index(date_type, length_one_index, kind): expected = 1 assert result == expected - result = length_one_index.get_slice_bound(date_type(1, 3, 1), "right", kind) + result = length_one_index.get_slice_bound( + date_type(1, 3, 1), "right", kind) expected = 1 assert result == expected @@ -501,7 +507,8 @@ def test_sel_date_scalar_pad(da, date_type, index, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", + "tolerance": timedelta(days=365)}], ) def test_sel_date_scalar_backfill(da, date_type, index, sel_kwargs): expected = xr.DataArray(3).assign_coords(time=index[2]) @@ -530,19 +537,26 @@ def test_sel_date_scalar_tolerance_raises(da, date_type, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "nearest"}, {"method": "nearest", "tolerance": timedelta(days=70)}], + [{"method": "nearest"}, {"method": "nearest", + "tolerance": timedelta(days=70)}], ) def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): - expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray([2, 3], coords=[[index[1], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 3], coords=[[index[1], index[2]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 11, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray( + [3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 11, 1), + date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) @@ -552,19 +566,24 @@ def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): [{"method": "pad"}, {"method": "pad", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_pad(da, date_type, index, sel_kwargs): - expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", + "tolerance": timedelta(days=365)}], ) def test_sel_date_list_backfill(da, date_type, index, sel_kwargs): - expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @@ -917,14 +936,35 @@ def test_cftimeindex_calendar_in_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str - assert " length=3" + assert " length=3" in repr_str + + +@requires_cftime +@pytest.mark.parametrize( + ("calendar", "expected"), + [ + ("noleap", "noleap"), + ("365_day", "noleap"), + ("360_day", "360_day"), + ("julian", "julian"), + ("gregorian", "gregorian"), + ("proleptic_gregorian", "proleptic_gregorian"), + ], +) +def test_cftimeindex_calendar_in_html_repr(calendar, expected): + index = xr.cftime_range(start="2000", periods=3, calendar=calendar) + index = xr.DataArray(index, dims='time') + with xr.set_options(display_style='html'): + repr_str = index.__repr__() + assert f" calendar='{expected}'" in repr_str @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap - strings = np.array([["2000-01-01", "2000-01-02"], ["2000-01-03", "2000-01-04"]]) + strings = np.array([["2000-01-01", "2000-01-02"], + ["2000-01-03", "2000-01-04"]]) expected = np.array( [ [DatetimeNoLeap(2000, 1, 1), DatetimeNoLeap(2000, 1, 2)], @@ -1080,5 +1120,6 @@ def test_asi8_distant_date(): date_type = cftime.DatetimeProlepticGregorian index = xr.CFTimeIndex([date_type(10731, 4, 22, 3, 25, 45, 123456)]) result = index.asi8 - expected = np.array([1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) + expected = np.array( + [1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) np.testing.assert_array_equal(result, expected) From bf8c5a06c3030d478384906b555b969715b70b2d Mon Sep 17 00:00:00 2001 From: AS Date: Sun, 7 Jun 2020 01:09:29 +0200 Subject: [PATCH 11/35] to_index() iff CFTimeIndex --- xarray/coding/cftimeindex.py | 36 ++++++----------- xarray/core/formatting.py | 57 +++++++++----------------- xarray/core/formatting_html.py | 12 ++---- xarray/tests/test_cftimeindex.py | 69 +++++++++++--------------------- 4 files changed, 58 insertions(+), 116 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 80842dbc293..4219eced91f 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -182,8 +182,7 @@ def f(self, min_cftime_version=min_cftime_version): raise ImportError( "The {!r} accessor requires a minimum " "version of cftime of {}. Found an " - "installed version of {}.".format( - name, min_cftime_version, version) + "installed version of {}.".format(name, min_cftime_version, version) ) f.__name__ = name @@ -239,13 +238,11 @@ class CFTimeIndex(pd.Index): hour = _field_accessor("hour", "The hours of the datetime") minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") - microsecond = _field_accessor( - "microsecond", "The microseconds of the datetime") + microsecond = _field_accessor("microsecond", "The microseconds of the datetime") dayofyear = _field_accessor( "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" ) - dayofweek = _field_accessor( - "dayofwk", "The day of week of the datetime", "1.0.2.1") + dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" ) @@ -337,8 +334,7 @@ def _partial_date_slice(self, resolution, parsed): Coordinates: * time (time) datetime64[ns] 2001-01-01T01:00:00 """ - start, end = _parsed_string_to_bounds( - self.date_type, resolution, parsed) + start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) times = self._data @@ -376,16 +372,13 @@ def _get_nearest_indexer(self, target, limit, tolerance): right_distances = abs(self.values[right_indexer] - target.values) if self.is_monotonic_increasing: - condition = (left_distances < right_distances) | ( - right_indexer == -1) + condition = (left_distances < right_distances) | (right_indexer == -1) else: - condition = (left_distances <= right_distances) | ( - right_indexer == -1) + condition = (left_distances <= right_distances) | (right_indexer == -1) indexer = np.where(condition, left_indexer, right_indexer) if tolerance is not None: - indexer = self._filter_indexer_tolerance( - target, indexer, tolerance) + indexer = self._filter_indexer_tolerance(target, indexer, tolerance) return indexer def _filter_indexer_tolerance(self, target, indexer, tolerance): @@ -408,10 +401,8 @@ def _maybe_cast_slice_bound(self, label, side, kind): """Adapted from pandas.tseries.index.DatetimeIndex._maybe_cast_slice_bound""" if isinstance(label, str): - parsed, resolution = _parse_iso8601_with_reso( - self.date_type, label) - start, end = _parsed_string_to_bounds( - self.date_type, resolution, parsed) + parsed, resolution = _parse_iso8601_with_reso(self.date_type, label) + start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) if self.is_monotonic_decreasing and len(self) > 1: return end if side == "left" else start return start if side == "left" else end @@ -615,8 +606,7 @@ def asi8(self): epoch = self.date_type(1970, 1, 1) return np.array( [ - _total_microseconds( - exact_cftime_datetime_difference(epoch, date)) + _total_microseconds(exact_cftime_datetime_difference(epoch, date)) for date in self.values ] ) @@ -742,8 +732,7 @@ def _cftimeindex_from_i8(values, date_type, name): CFTimeIndex """ epoch = date_type(1970, 1, 1) - dates = np.array([epoch + timedelta(microseconds=int(value)) - for value in values]) + dates = np.array([epoch + timedelta(microseconds=int(value)) for value in values]) return CFTimeIndex(dates, name=name) @@ -778,8 +767,7 @@ def _round_to_nearest_half_even(values, unit): return _ceil_int(values - unit // 2, unit) quotient, remainder = np.divmod(values, unit) mask = np.logical_or( - remainder > (unit // 2), np.logical_and(remainder == - (unit // 2), quotient % 2) + remainder > (unit // 2), np.logical_and(remainder == (unit // 2), quotient % 2) ) quotient[mask] += 1 return quotient * unit diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 289f2634fc6..7ffdc6c1d14 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -65,8 +65,7 @@ def first_n_items(array, n_desired): return [] if n_desired < array.size: - indexer = _get_indexer_at_least_n_items( - array.shape, n_desired, from_end=False) + indexer = _get_indexer_at_least_n_items(array.shape, n_desired, from_end=False) array = array[indexer] return np.asarray(array).flat[:n_desired] @@ -81,8 +80,7 @@ def last_n_items(array, n_desired): return [] if n_desired < array.size: - indexer = _get_indexer_at_least_n_items( - array.shape, n_desired, from_end=True) + indexer = _get_indexer_at_least_n_items(array.shape, n_desired, from_end=True) array = array[indexer] return np.asarray(array).flat[-n_desired:] @@ -154,8 +152,7 @@ def format_items(x): timedelta_format = "datetime" if np.issubdtype(x.dtype, np.timedelta64): x = np.asarray(x, dtype="timedelta64[ns]") - day_part = x[~pd.isnull(x)].astype( - "timedelta64[D]").astype("timedelta64[ns]") + day_part = x[~pd.isnull(x)].astype("timedelta64[D]").astype("timedelta64[ns]") time_needed = x[~pd.isnull(x)] != day_part day_needed = day_part != np.timedelta64(0, "ns") if np.logical_not(day_needed).all(): @@ -179,8 +176,7 @@ def format_array_flat(array, max_width: int): relevant_front_items = format_items( first_n_items(array, (max_possibly_relevant + 1) // 2) ) - relevant_back_items = format_items( - last_n_items(array, max_possibly_relevant // 2)) + relevant_back_items = format_items(last_n_items(array, max_possibly_relevant // 2)) # interleave relevant front and back items: # [a, b, c] and [y, z] -> [a, z, b, y, c] relevant_items = sum( @@ -193,8 +189,7 @@ def format_array_flat(array, max_width: int): ): padding = " ... " count = min( - array.size, max( - np.argmax(cum_len + len(padding) - 1 > max_width), 2) + array.size, max(np.argmax(cum_len + len(padding) - 1 > max_width), 2) ) else: count = array.size @@ -280,8 +275,7 @@ def summarize_variable( if max_width is None: max_width_options = OPTIONS["display_width"] if not isinstance(max_width_options, int): - raise TypeError( - f"`max_width` value of `{max_width}` is not a valid int") + raise TypeError(f"`max_width` value of `{max_width}` is not a valid int") else: max_width = max_width_options first_col = pretty_print(f" {marker} {name} ", col_width) @@ -433,8 +427,7 @@ def short_numpy_repr(array): # default to lower precision so a full (abbreviated) line can fit on # one line with the default display_width - options = {"precision": 6, - "linewidth": OPTIONS["display_width"], "threshold": 200} + options = {"precision": 6, "linewidth": OPTIONS["display_width"], "threshold": 200} if array.ndim < 3: edgeitems = 3 elif array.ndim == 3: @@ -449,21 +442,12 @@ def short_numpy_repr(array): def short_data_repr(array): """Format "data" for DataArray and Variable.""" from ..coding.cftimeindex import CFTimeIndex - # try to convert to index: needed to get CFTimeIndex in html repr - try: - array2 = array.to_index() - # doesnt catch I dont understand why - assert isinstance(array2, CFTimeIndex) - if not isinstance(array2, CFTimeIndex): - print('revert') - array2 = array - except ValueError: # catch to_index() fails - array2 = array - except AssertionError: # catch other types than CFTimeIndex - array2 = array - except: - array2 = array - array = array2 + + # try to convert to index only if CFTimeIndex: needed for html repr + if hasattr(array, "to_index") and hasattr(array, "variable"): + if hasattr(array.variable._data, "array"): + if isinstance(array.variable._data.array, CFTimeIndex): + array = array.to_index() internal_data = getattr(array, "variable", array)._data if isinstance(array, np.ndarray): @@ -489,8 +473,7 @@ def array_repr(arr): name_str = "" summary = [ - "".format( - type(arr).__name__, name_str, dim_summary(arr)), + "".format(type(arr).__name__, name_str, dim_summary(arr)), short_data_repr(arr), ] @@ -596,8 +579,7 @@ def extra_items_repr(extra_keys, mapping, ab_side): for var_s, attr_s in zip(temp, attrs_summary) ] - diff_items += [ab_side + s[1:] - for ab_side, s in zip(("L", "R"), temp)] + diff_items += [ab_side + s[1:] for ab_side, s in zip(("L", "R"), temp)] if diff_items: summary += ["Differing {}:".format(title.lower())] + diff_items @@ -641,8 +623,7 @@ def diff_array_repr(a, b, compat): summary.append(diff_dim_summary(a, b)) if not array_equiv(a.data, b.data): - temp = [wrap_indent(short_numpy_repr(obj), start=" ") - for obj in (a, b)] + temp = [wrap_indent(short_numpy_repr(obj), start=" ") for obj in (a, b)] diff_data_repr = [ ab_side + "\n" + ab_data_repr for ab_side, ab_data_repr in zip(("L", "R"), temp) @@ -673,11 +654,9 @@ def diff_dataset_repr(a, b, compat): ) summary.append(diff_dim_summary(a, b)) - summary.append(diff_coords_repr( - a.coords, b.coords, compat, col_width=col_width)) + summary.append(diff_coords_repr(a.coords, b.coords, compat, col_width=col_width)) summary.append( - diff_data_vars_repr(a.data_vars, b.data_vars, - compat, col_width=col_width) + diff_data_vars_repr(a.data_vars, b.data_vars, compat, col_width=col_width) ) if compat == "identical": diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index a692aeb9ca0..de43ce40a17 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -8,13 +8,11 @@ from .formatting import inline_variable_array_repr, short_data_repr CSS_FILE_PATH = "/".join(("static", "css", "style.css")) -CSS_STYLE = pkg_resources.resource_string( - "xarray", CSS_FILE_PATH).decode("utf8") +CSS_STYLE = pkg_resources.resource_string("xarray", CSS_FILE_PATH).decode("utf8") ICONS_SVG_PATH = "/".join(("static", "html", "icons-svg-inline.html")) -ICONS_SVG = pkg_resources.resource_string( - "xarray", ICONS_SVG_PATH).decode("utf8") +ICONS_SVG = pkg_resources.resource_string("xarray", ICONS_SVG_PATH).decode("utf8") def short_data_repr_html(array): @@ -87,8 +85,7 @@ def summarize_coords(variables): for k, v in variables.items(): coords.update(**summarize_coord(k, v)) - vars_li = "".join( - f"
  • {v}
  • " for v in coords.values()) + vars_li = "".join(f"
  • {v}
  • " for v in coords.values()) return f"
      {vars_li}
    " @@ -231,8 +228,7 @@ def _obj_repr(obj, header_components, sections): """ header = f"
    {''.join(h for h in header_components)}
    " - sections = "".join( - f"
  • {s}
  • " for s in sections) + sections = "".join(f"
  • {s}
  • " for s in sections) return ( "
    " diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 9c0e8620ab3..b169bdd69cf 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -194,8 +194,7 @@ def test_assert_all_valid_date_type(date_type, index): with pytest.raises(TypeError): assert_all_valid_date_type(np.array([1, date_type(1, 1, 1)])) - assert_all_valid_date_type( - np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) + assert_all_valid_date_type(np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) @requires_cftime @@ -262,8 +261,7 @@ def test_parse_string_to_bounds_year(date_type, dec_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 1, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "year", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "year", parsed) assert result_start == expected_start assert result_end == expected_end @@ -273,8 +271,7 @@ def test_parse_string_to_bounds_month_feb(date_type, feb_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 2, 1) expected_end = date_type(2, 2, feb_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -284,8 +281,7 @@ def test_parse_string_to_bounds_month_dec(date_type, dec_days): parsed = date_type(2, 12, 1) expected_start = date_type(2, 12, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -307,8 +303,7 @@ def test_parsed_string_to_bounds_sub_monthly( expected_start = date_type(*ex_start_args) expected_end = date_type(*ex_end_args) - result_start, result_end = _parsed_string_to_bounds( - date_type, reso, parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, reso, parsed) assert result_start == expected_start assert result_end == expected_end @@ -393,8 +388,7 @@ def test_get_slice_bound_length_one_index(date_type, length_one_index, kind): expected = 1 assert result == expected - result = length_one_index.get_slice_bound( - date_type(1, 3, 1), "right", kind) + result = length_one_index.get_slice_bound(date_type(1, 3, 1), "right", kind) expected = 1 assert result == expected @@ -507,8 +501,7 @@ def test_sel_date_scalar_pad(da, date_type, index, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", - "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], ) def test_sel_date_scalar_backfill(da, date_type, index, sel_kwargs): expected = xr.DataArray(3).assign_coords(time=index[2]) @@ -537,26 +530,19 @@ def test_sel_date_scalar_tolerance_raises(da, date_type, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "nearest"}, {"method": "nearest", - "tolerance": timedelta(days=70)}], + [{"method": "nearest"}, {"method": "nearest", "tolerance": timedelta(days=70)}], ) def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray( - [2, 3], coords=[[index[1], index[2]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray([2, 3], coords=[[index[1], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray( - [3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 11, 1), - date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 11, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) @@ -566,24 +552,19 @@ def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): [{"method": "pad"}, {"method": "pad", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_pad(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", - "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_backfill(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @@ -951,10 +932,10 @@ def test_cftimeindex_calendar_in_repr(calendar, expected): ("proleptic_gregorian", "proleptic_gregorian"), ], ) -def test_cftimeindex_calendar_in_html_repr(calendar, expected): +def test_cftimeindex_calendar_in_dataArray_html_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) - index = xr.DataArray(index, dims='time') - with xr.set_options(display_style='html'): + index = xr.DataArray(index, dims="time") + with xr.set_options(display_style="html"): repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str @@ -963,8 +944,7 @@ def test_cftimeindex_calendar_in_html_repr(calendar, expected): def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap - strings = np.array([["2000-01-01", "2000-01-02"], - ["2000-01-03", "2000-01-04"]]) + strings = np.array([["2000-01-01", "2000-01-02"], ["2000-01-03", "2000-01-04"]]) expected = np.array( [ [DatetimeNoLeap(2000, 1, 1), DatetimeNoLeap(2000, 1, 2)], @@ -1120,6 +1100,5 @@ def test_asi8_distant_date(): date_type = cftime.DatetimeProlepticGregorian index = xr.CFTimeIndex([date_type(10731, 4, 22, 3, 25, 45, 123456)]) result = index.asi8 - expected = np.array( - [1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) + expected = np.array([1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) np.testing.assert_array_equal(result, expected) From 1e809d033bc80b17737fc0811529d079ef134cec Mon Sep 17 00:00:00 2001 From: AS Date: Sun, 7 Jun 2020 01:17:55 +0200 Subject: [PATCH 12/35] revert linting --- xarray/coding/cftimeindex.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 4219eced91f..babfc46aa2c 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -239,9 +239,7 @@ class CFTimeIndex(pd.Index): minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") microsecond = _field_accessor("microsecond", "The microseconds of the datetime") - dayofyear = _field_accessor( - "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" - ) + dayofyear = _field_accessor("dayofyr", "The ordinal day of year of the datetime", "1.0.2.1") dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" From 1bfdc7c489f2f9e41e38f9bb29c02f5f801ff259 Mon Sep 17 00:00:00 2001 From: AS Date: Sun, 7 Jun 2020 01:20:09 +0200 Subject: [PATCH 13/35] revert linting --- xarray/coding/cftimeindex.py | 4 +++- xarray/core/formatting_html.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index babfc46aa2c..4219eced91f 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -239,7 +239,9 @@ class CFTimeIndex(pd.Index): minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") microsecond = _field_accessor("microsecond", "The microseconds of the datetime") - dayofyear = _field_accessor("dayofyr", "The ordinal day of year of the datetime", "1.0.2.1") + dayofyear = _field_accessor( + "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" + ) dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index de43ce40a17..d525732c58a 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -86,6 +86,7 @@ def summarize_coords(variables): coords.update(**summarize_coord(k, v)) vars_li = "".join(f"
  • {v}
  • " for v in coords.values()) + return f"
      {vars_li}
    " From d58cb7dd75fb326dc083a7bfbd03978f48b2ea79 Mon Sep 17 00:00:00 2001 From: AS Date: Sun, 7 Jun 2020 01:21:11 +0200 Subject: [PATCH 14/35] revert linting --- xarray/core/formatting_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index d525732c58a..69832d6ca3d 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -86,7 +86,7 @@ def summarize_coords(variables): coords.update(**summarize_coord(k, v)) vars_li = "".join(f"
  • {v}
  • " for v in coords.values()) - + return f"
      {vars_li}
    " From a0d00ab72ccbdd0a7abb8da68dbfc70e3e10bf5f Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 9 Jun 2020 12:01:38 +0200 Subject: [PATCH 15/35] to_index in short_data_repr_html --- xarray/core/formatting.py | 19 +++++++++++++------ xarray/core/formatting_html.py | 8 ++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 7ffdc6c1d14..0218875d7fe 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -439,21 +439,28 @@ def short_numpy_repr(array): return repr(array) -def short_data_repr(array): - """Format "data" for DataArray and Variable.""" +def _maybe_convert_to_index(array): + # try to convert to index only if CFTimeIndex: needed for html repr from ..coding.cftimeindex import CFTimeIndex - # try to convert to index only if CFTimeIndex: needed for html repr if hasattr(array, "to_index") and hasattr(array, "variable"): if hasattr(array.variable._data, "array"): if isinstance(array.variable._data.array, CFTimeIndex): + print("convert to index") array = array.to_index() + return array + +def short_data_repr(array): + """Format "data" for DataArray and Variable.""" + from ..coding.cftimeindex import CFTimeIndex + + array = _maybe_convert_to_index(array) internal_data = getattr(array, "variable", array)._data - if isinstance(array, np.ndarray): - return short_numpy_repr(array) - elif isinstance(array, CFTimeIndex): + if isinstance(array, CFTimeIndex): return repr(array) + elif isinstance(array, np.ndarray): + return short_numpy_repr(array) elif hasattr(internal_data, "__array_function__") or isinstance( internal_data, dask_array_type ): diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 69832d6ca3d..5a008064ce4 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -18,6 +18,14 @@ def short_data_repr_html(array): """Format "data" for DataArray and Variable.""" internal_data = getattr(array, "variable", array)._data + from .indexing import PandasIndexAdapter + + if isinstance(internal_data, PandasIndexAdapter): + array = array.to_index() + from ..coding.cftimeindex import CFTimeIndex + + if isinstance(array, CFTimeIndex): + return repr(array) if hasattr(internal_data, "_repr_html_"): return internal_data._repr_html_() return escape(short_data_repr(array)) From 019d309f375a4a12b99a237910c65283bc2b589d Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 9 Jun 2020 13:28:00 +0200 Subject: [PATCH 16/35] refine test and rm prints --- xarray/core/formatting.py | 3 +-- xarray/tests/test_cftimeindex.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 0218875d7fe..f0d9a31f13b 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -440,13 +440,12 @@ def short_numpy_repr(array): def _maybe_convert_to_index(array): - # try to convert to index only if CFTimeIndex: needed for html repr + """convert to index only if CFTimeIndex: needed for html repr""" from ..coding.cftimeindex import CFTimeIndex if hasattr(array, "to_index") and hasattr(array, "variable"): if hasattr(array.variable._data, "array"): if isinstance(array.variable._data.array, CFTimeIndex): - print("convert to index") array = array.to_index() return array diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index b169bdd69cf..6474feedf6d 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -913,7 +913,7 @@ def test_cftimeindex_calendar_property(calendar, expected): ("proleptic_gregorian", "proleptic_gregorian"), ], ) -def test_cftimeindex_calendar_in_repr(calendar, expected): +def test_cftimeindex_calendar_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str @@ -932,11 +932,15 @@ def test_cftimeindex_calendar_in_repr(calendar, expected): ("proleptic_gregorian", "proleptic_gregorian"), ], ) -def test_cftimeindex_calendar_in_dataArray_html_repr(calendar, expected): - index = xr.cftime_range(start="2000", periods=3, calendar=calendar) - index = xr.DataArray(index, dims="time") - with xr.set_options(display_style="html"): - repr_str = index.__repr__() +@pytest.mark.parametrize("display_style", ["html", "text"]) +def test_cftimeindex_calendar_as_coord_in_dataArray_repr( + calendar, expected, display_style +): + index = xr.DataArray( + xr.cftime_range(start="2000", periods=3, calendar=calendar), dims="time" + ) + with xr.set_options(display_style=display_style): + repr_str = index.time.__repr__() assert f" calendar='{expected}'" in repr_str From 269e96765570be40a5e1bfb36692ff7200cbf1c0 Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 9 Jun 2020 13:44:07 +0200 Subject: [PATCH 17/35] fix to pass all tests --- xarray/core/formatting_html.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 5a008064ce4..04bf215f8af 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -17,13 +17,14 @@ def short_data_repr_html(array): """Format "data" for DataArray and Variable.""" - internal_data = getattr(array, "variable", array)._data from .indexing import PandasIndexAdapter - - if isinstance(internal_data, PandasIndexAdapter): - array = array.to_index() from ..coding.cftimeindex import CFTimeIndex + internal_data = getattr(array, "variable", array)._data + # try to convert internal_data.array to CFTimeIndex + if isinstance(internal_data, PandasIndexAdapter): + if isinstance(internal_data.array, CFTimeIndex): + array = array.to_index() if isinstance(array, CFTimeIndex): return repr(array) if hasattr(internal_data, "_repr_html_"): From 68a37c6af1e0d4a0b489c47085dc685e8917d0ae Mon Sep 17 00:00:00 2001 From: AS Date: Thu, 11 Jun 2020 15:32:36 +0200 Subject: [PATCH 18/35] revert linting changes --- xarray/coding/cftimeindex.py | 38 ++++++++++++------- xarray/tests/test_cftimeindex.py | 64 +++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 4219eced91f..f248f93b6b0 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -182,7 +182,8 @@ def f(self, min_cftime_version=min_cftime_version): raise ImportError( "The {!r} accessor requires a minimum " "version of cftime of {}. Found an " - "installed version of {}.".format(name, min_cftime_version, version) + "installed version of {}.".format( + name, min_cftime_version, version) ) f.__name__ = name @@ -238,11 +239,13 @@ class CFTimeIndex(pd.Index): hour = _field_accessor("hour", "The hours of the datetime") minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") - microsecond = _field_accessor("microsecond", "The microseconds of the datetime") + microsecond = _field_accessor( + "microsecond", "The microseconds of the datetime") dayofyear = _field_accessor( "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" ) - dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") + dayofweek = _field_accessor( + "dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" ) @@ -273,8 +276,6 @@ def __repr__(self): klass_name = type(self).__name__ data = self._format_data() attrs = self._format_attrs() - # add length to attrs - attrs.append(("length", f"{len(self)}")) # add calendar to attrs attrs.append(("calendar", f"'{self.calendar}'")) space = self._format_space() @@ -334,7 +335,8 @@ def _partial_date_slice(self, resolution, parsed): Coordinates: * time (time) datetime64[ns] 2001-01-01T01:00:00 """ - start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) + start, end = _parsed_string_to_bounds( + self.date_type, resolution, parsed) times = self._data @@ -372,13 +374,16 @@ def _get_nearest_indexer(self, target, limit, tolerance): right_distances = abs(self.values[right_indexer] - target.values) if self.is_monotonic_increasing: - condition = (left_distances < right_distances) | (right_indexer == -1) + condition = (left_distances < right_distances) | ( + right_indexer == -1) else: - condition = (left_distances <= right_distances) | (right_indexer == -1) + condition = (left_distances <= right_distances) | ( + right_indexer == -1) indexer = np.where(condition, left_indexer, right_indexer) if tolerance is not None: - indexer = self._filter_indexer_tolerance(target, indexer, tolerance) + indexer = self._filter_indexer_tolerance( + target, indexer, tolerance) return indexer def _filter_indexer_tolerance(self, target, indexer, tolerance): @@ -401,8 +406,10 @@ def _maybe_cast_slice_bound(self, label, side, kind): """Adapted from pandas.tseries.index.DatetimeIndex._maybe_cast_slice_bound""" if isinstance(label, str): - parsed, resolution = _parse_iso8601_with_reso(self.date_type, label) - start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) + parsed, resolution = _parse_iso8601_with_reso( + self.date_type, label) + start, end = _parsed_string_to_bounds( + self.date_type, resolution, parsed) if self.is_monotonic_decreasing and len(self) > 1: return end if side == "left" else start return start if side == "left" else end @@ -606,7 +613,8 @@ def asi8(self): epoch = self.date_type(1970, 1, 1) return np.array( [ - _total_microseconds(exact_cftime_datetime_difference(epoch, date)) + _total_microseconds( + exact_cftime_datetime_difference(epoch, date)) for date in self.values ] ) @@ -732,7 +740,8 @@ def _cftimeindex_from_i8(values, date_type, name): CFTimeIndex """ epoch = date_type(1970, 1, 1) - dates = np.array([epoch + timedelta(microseconds=int(value)) for value in values]) + dates = np.array([epoch + timedelta(microseconds=int(value)) + for value in values]) return CFTimeIndex(dates, name=name) @@ -767,7 +776,8 @@ def _round_to_nearest_half_even(values, unit): return _ceil_int(values - unit // 2, unit) quotient, remainder = np.divmod(values, unit) mask = np.logical_or( - remainder > (unit // 2), np.logical_and(remainder == (unit // 2), quotient % 2) + remainder > (unit // 2), np.logical_and(remainder == + (unit // 2), quotient % 2) ) quotient[mask] += 1 return quotient * unit diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 6474feedf6d..995fe72f54c 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -194,7 +194,8 @@ def test_assert_all_valid_date_type(date_type, index): with pytest.raises(TypeError): assert_all_valid_date_type(np.array([1, date_type(1, 1, 1)])) - assert_all_valid_date_type(np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) + assert_all_valid_date_type( + np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) @requires_cftime @@ -261,7 +262,8 @@ def test_parse_string_to_bounds_year(date_type, dec_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 1, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "year", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "year", parsed) assert result_start == expected_start assert result_end == expected_end @@ -271,7 +273,8 @@ def test_parse_string_to_bounds_month_feb(date_type, feb_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 2, 1) expected_end = date_type(2, 2, feb_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -281,7 +284,8 @@ def test_parse_string_to_bounds_month_dec(date_type, dec_days): parsed = date_type(2, 12, 1) expected_start = date_type(2, 12, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -303,7 +307,8 @@ def test_parsed_string_to_bounds_sub_monthly( expected_start = date_type(*ex_start_args) expected_end = date_type(*ex_end_args) - result_start, result_end = _parsed_string_to_bounds(date_type, reso, parsed) + result_start, result_end = _parsed_string_to_bounds( + date_type, reso, parsed) assert result_start == expected_start assert result_end == expected_end @@ -388,7 +393,8 @@ def test_get_slice_bound_length_one_index(date_type, length_one_index, kind): expected = 1 assert result == expected - result = length_one_index.get_slice_bound(date_type(1, 3, 1), "right", kind) + result = length_one_index.get_slice_bound( + date_type(1, 3, 1), "right", kind) expected = 1 assert result == expected @@ -501,7 +507,8 @@ def test_sel_date_scalar_pad(da, date_type, index, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", + "tolerance": timedelta(days=365)}], ) def test_sel_date_scalar_backfill(da, date_type, index, sel_kwargs): expected = xr.DataArray(3).assign_coords(time=index[2]) @@ -530,19 +537,26 @@ def test_sel_date_scalar_tolerance_raises(da, date_type, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "nearest"}, {"method": "nearest", "tolerance": timedelta(days=70)}], + [{"method": "nearest"}, {"method": "nearest", + "tolerance": timedelta(days=70)}], ) def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): - expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray([2, 3], coords=[[index[1], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 3], coords=[[index[1], index[2]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 11, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray( + [3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 11, 1), + date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) @@ -552,19 +566,24 @@ def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): [{"method": "pad"}, {"method": "pad", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_pad(da, date_type, index, sel_kwargs): - expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", + "tolerance": timedelta(days=365)}], ) def test_sel_date_list_backfill(da, date_type, index, sel_kwargs): - expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray( + [3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel( + time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @@ -917,7 +936,6 @@ def test_cftimeindex_calendar_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str - assert " length=3" in repr_str @requires_cftime @@ -948,7 +966,8 @@ def test_cftimeindex_calendar_as_coord_in_dataArray_repr( def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap - strings = np.array([["2000-01-01", "2000-01-02"], ["2000-01-03", "2000-01-04"]]) + strings = np.array([["2000-01-01", "2000-01-02"], + ["2000-01-03", "2000-01-04"]]) expected = np.array( [ [DatetimeNoLeap(2000, 1, 1), DatetimeNoLeap(2000, 1, 2)], @@ -1104,5 +1123,6 @@ def test_asi8_distant_date(): date_type = cftime.DatetimeProlepticGregorian index = xr.CFTimeIndex([date_type(10731, 4, 22, 3, 25, 45, 123456)]) result = index.asi8 - expected = np.array([1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) + expected = np.array( + [1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) np.testing.assert_array_equal(result, expected) From 0907ceff6ba9d2b525fdbb25ed1fd387f1227e20 Mon Sep 17 00:00:00 2001 From: AS Date: Fri, 12 Jun 2020 10:53:07 +0200 Subject: [PATCH 19/35] revert to_index() --- doc/whats-new.rst | 4 +- xarray/coding/cftimeindex.py | 42 +++++---------- xarray/core/formatting.py | 18 +------ xarray/core/formatting_html.py | 9 ---- xarray/tests/test_cftimeindex.py | 92 ++++++++++---------------------- 5 files changed, 45 insertions(+), 120 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index b496b709aaa..378d2ccac1e 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -82,8 +82,8 @@ New Features :py:func:`xarray.decode_cf`) that allows to disable/enable the decoding of timedeltas independently of time decoding (:issue:`1621`) `Aureliana Barghini ` -- Add ``calendar`` as a new property for ``cftimeindex`` and show in ``calendar`` and - ``length`` in ``__repr__`` (:issue:`2416`, :pull:`4092`) +- Add ``calendar`` as a new property for ``CFTimeIndex`` and show in ``calendar`` and + ``length`` in ``CFTimeIndex.__repr__`` (:issue:`2416`, :pull:`4092`) `Aaron Spring ` Bug fixes diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index f248f93b6b0..4f138965440 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -182,8 +182,7 @@ def f(self, min_cftime_version=min_cftime_version): raise ImportError( "The {!r} accessor requires a minimum " "version of cftime of {}. Found an " - "installed version of {}.".format( - name, min_cftime_version, version) + "installed version of {}.".format(name, min_cftime_version, version) ) f.__name__ = name @@ -239,13 +238,11 @@ class CFTimeIndex(pd.Index): hour = _field_accessor("hour", "The hours of the datetime") minute = _field_accessor("minute", "The minutes of the datetime") second = _field_accessor("second", "The seconds of the datetime") - microsecond = _field_accessor( - "microsecond", "The microseconds of the datetime") + microsecond = _field_accessor("microsecond", "The microseconds of the datetime") dayofyear = _field_accessor( "dayofyr", "The ordinal day of year of the datetime", "1.0.2.1" ) - dayofweek = _field_accessor( - "dayofwk", "The day of week of the datetime", "1.0.2.1") + dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1") days_in_month = _field_accessor( "daysinmonth", "The number of days in the month of the datetime", "1.1.0.0" ) @@ -262,10 +259,6 @@ def __new__(cls, data, name=None): result._cache = {} return result - @property - def data(self): - return self.values - def __repr__(self): """ Return a string representation for this object. @@ -276,6 +269,8 @@ def __repr__(self): klass_name = type(self).__name__ data = self._format_data() attrs = self._format_attrs() + # add length to attrs + attrs.append(("length", f"'{len(self)}'")) # add calendar to attrs attrs.append(("calendar", f"'{self.calendar}'")) space = self._format_space() @@ -335,8 +330,7 @@ def _partial_date_slice(self, resolution, parsed): Coordinates: * time (time) datetime64[ns] 2001-01-01T01:00:00 """ - start, end = _parsed_string_to_bounds( - self.date_type, resolution, parsed) + start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) times = self._data @@ -374,16 +368,13 @@ def _get_nearest_indexer(self, target, limit, tolerance): right_distances = abs(self.values[right_indexer] - target.values) if self.is_monotonic_increasing: - condition = (left_distances < right_distances) | ( - right_indexer == -1) + condition = (left_distances < right_distances) | (right_indexer == -1) else: - condition = (left_distances <= right_distances) | ( - right_indexer == -1) + condition = (left_distances <= right_distances) | (right_indexer == -1) indexer = np.where(condition, left_indexer, right_indexer) if tolerance is not None: - indexer = self._filter_indexer_tolerance( - target, indexer, tolerance) + indexer = self._filter_indexer_tolerance(target, indexer, tolerance) return indexer def _filter_indexer_tolerance(self, target, indexer, tolerance): @@ -406,10 +397,8 @@ def _maybe_cast_slice_bound(self, label, side, kind): """Adapted from pandas.tseries.index.DatetimeIndex._maybe_cast_slice_bound""" if isinstance(label, str): - parsed, resolution = _parse_iso8601_with_reso( - self.date_type, label) - start, end = _parsed_string_to_bounds( - self.date_type, resolution, parsed) + parsed, resolution = _parse_iso8601_with_reso(self.date_type, label) + start, end = _parsed_string_to_bounds(self.date_type, resolution, parsed) if self.is_monotonic_decreasing and len(self) > 1: return end if side == "left" else start return start if side == "left" else end @@ -613,8 +602,7 @@ def asi8(self): epoch = self.date_type(1970, 1, 1) return np.array( [ - _total_microseconds( - exact_cftime_datetime_difference(epoch, date)) + _total_microseconds(exact_cftime_datetime_difference(epoch, date)) for date in self.values ] ) @@ -740,8 +728,7 @@ def _cftimeindex_from_i8(values, date_type, name): CFTimeIndex """ epoch = date_type(1970, 1, 1) - dates = np.array([epoch + timedelta(microseconds=int(value)) - for value in values]) + dates = np.array([epoch + timedelta(microseconds=int(value)) for value in values]) return CFTimeIndex(dates, name=name) @@ -776,8 +763,7 @@ def _round_to_nearest_half_even(values, unit): return _ceil_int(values - unit // 2, unit) quotient, remainder = np.divmod(values, unit) mask = np.logical_or( - remainder > (unit // 2), np.logical_and(remainder == - (unit // 2), quotient % 2) + remainder > (unit // 2), np.logical_and(remainder == (unit // 2), quotient % 2) ) quotient[mask] += 1 return quotient * unit diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index f0d9a31f13b..d6732fc182e 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -439,26 +439,10 @@ def short_numpy_repr(array): return repr(array) -def _maybe_convert_to_index(array): - """convert to index only if CFTimeIndex: needed for html repr""" - from ..coding.cftimeindex import CFTimeIndex - - if hasattr(array, "to_index") and hasattr(array, "variable"): - if hasattr(array.variable._data, "array"): - if isinstance(array.variable._data.array, CFTimeIndex): - array = array.to_index() - return array - - def short_data_repr(array): """Format "data" for DataArray and Variable.""" - from ..coding.cftimeindex import CFTimeIndex - - array = _maybe_convert_to_index(array) internal_data = getattr(array, "variable", array)._data - if isinstance(array, CFTimeIndex): - return repr(array) - elif isinstance(array, np.ndarray): + if isinstance(array, np.ndarray): return short_numpy_repr(array) elif hasattr(internal_data, "__array_function__") or isinstance( internal_data, dask_array_type diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index 04bf215f8af..69832d6ca3d 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -17,16 +17,7 @@ def short_data_repr_html(array): """Format "data" for DataArray and Variable.""" - from .indexing import PandasIndexAdapter - from ..coding.cftimeindex import CFTimeIndex - internal_data = getattr(array, "variable", array)._data - # try to convert internal_data.array to CFTimeIndex - if isinstance(internal_data, PandasIndexAdapter): - if isinstance(internal_data.array, CFTimeIndex): - array = array.to_index() - if isinstance(array, CFTimeIndex): - return repr(array) if hasattr(internal_data, "_repr_html_"): return internal_data._repr_html_() return escape(short_data_repr(array)) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 995fe72f54c..c6e21738329 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -194,8 +194,7 @@ def test_assert_all_valid_date_type(date_type, index): with pytest.raises(TypeError): assert_all_valid_date_type(np.array([1, date_type(1, 1, 1)])) - assert_all_valid_date_type( - np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) + assert_all_valid_date_type(np.array([date_type(1, 1, 1), date_type(1, 2, 1)])) @requires_cftime @@ -262,8 +261,7 @@ def test_parse_string_to_bounds_year(date_type, dec_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 1, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "year", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "year", parsed) assert result_start == expected_start assert result_end == expected_end @@ -273,8 +271,7 @@ def test_parse_string_to_bounds_month_feb(date_type, feb_days): parsed = date_type(2, 2, 10, 6, 2, 8, 1) expected_start = date_type(2, 2, 1) expected_end = date_type(2, 2, feb_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -284,8 +281,7 @@ def test_parse_string_to_bounds_month_dec(date_type, dec_days): parsed = date_type(2, 12, 1) expected_start = date_type(2, 12, 1) expected_end = date_type(2, 12, dec_days, 23, 59, 59, 999999) - result_start, result_end = _parsed_string_to_bounds( - date_type, "month", parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, "month", parsed) assert result_start == expected_start assert result_end == expected_end @@ -307,8 +303,7 @@ def test_parsed_string_to_bounds_sub_monthly( expected_start = date_type(*ex_start_args) expected_end = date_type(*ex_end_args) - result_start, result_end = _parsed_string_to_bounds( - date_type, reso, parsed) + result_start, result_end = _parsed_string_to_bounds(date_type, reso, parsed) assert result_start == expected_start assert result_end == expected_end @@ -393,8 +388,7 @@ def test_get_slice_bound_length_one_index(date_type, length_one_index, kind): expected = 1 assert result == expected - result = length_one_index.get_slice_bound( - date_type(1, 3, 1), "right", kind) + result = length_one_index.get_slice_bound(date_type(1, 3, 1), "right", kind) expected = 1 assert result == expected @@ -507,8 +501,7 @@ def test_sel_date_scalar_pad(da, date_type, index, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", - "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], ) def test_sel_date_scalar_backfill(da, date_type, index, sel_kwargs): expected = xr.DataArray(3).assign_coords(time=index[2]) @@ -537,26 +530,19 @@ def test_sel_date_scalar_tolerance_raises(da, date_type, sel_kwargs): @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "nearest"}, {"method": "nearest", - "tolerance": timedelta(days=70)}], + [{"method": "nearest"}, {"method": "nearest", "tolerance": timedelta(days=70)}], ) def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray( - [2, 3], coords=[[index[1], index[2]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray([2, 3], coords=[[index[1], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) - expected = xr.DataArray( - [3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel(time=[date_type(1, 11, 1), - date_type(1, 12, 1)], **sel_kwargs) + expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 11, 1), date_type(1, 12, 1)], **sel_kwargs) assert_identical(result, expected) @@ -566,24 +552,19 @@ def test_sel_date_list_nearest(da, date_type, index, sel_kwargs): [{"method": "pad"}, {"method": "pad", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_pad(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [2, 2], coords=[[index[1], index[1]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([2, 2], coords=[[index[1], index[1]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @requires_cftime @pytest.mark.parametrize( "sel_kwargs", - [{"method": "backfill"}, {"method": "backfill", - "tolerance": timedelta(days=365)}], + [{"method": "backfill"}, {"method": "backfill", "tolerance": timedelta(days=365)}], ) def test_sel_date_list_backfill(da, date_type, index, sel_kwargs): - expected = xr.DataArray( - [3, 3], coords=[[index[2], index[2]]], dims=["time"]) - result = da.sel( - time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) + expected = xr.DataArray([3, 3], coords=[[index[2], index[2]]], dims=["time"]) + result = da.sel(time=[date_type(1, 3, 1), date_type(1, 4, 1)], **sel_kwargs) assert_identical(result, expected) @@ -933,41 +914,25 @@ def test_cftimeindex_calendar_property(calendar, expected): ], ) def test_cftimeindex_calendar_repr(calendar, expected): + """Test that cftimeindex has calendar property in repr.""" index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str -@requires_cftime -@pytest.mark.parametrize( - ("calendar", "expected"), - [ - ("noleap", "noleap"), - ("365_day", "noleap"), - ("360_day", "360_day"), - ("julian", "julian"), - ("gregorian", "gregorian"), - ("proleptic_gregorian", "proleptic_gregorian"), - ], -) -@pytest.mark.parametrize("display_style", ["html", "text"]) -def test_cftimeindex_calendar_as_coord_in_dataArray_repr( - calendar, expected, display_style -): - index = xr.DataArray( - xr.cftime_range(start="2000", periods=3, calendar=calendar), dims="time" - ) - with xr.set_options(display_style=display_style): - repr_str = index.time.__repr__() - assert f" calendar='{expected}'" in repr_str +@pytest.mark.parametrize("periods", [2, 4]) +def test_cftimeindex_periods_repr(periods): + """Test that cftimeindex has periods property in repr.""" + index = xr.cftime_range(start="2000", periods=periods) + repr_str = index.__repr__() + assert f" length='{periods}'" in repr_str @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap - strings = np.array([["2000-01-01", "2000-01-02"], - ["2000-01-03", "2000-01-04"]]) + strings = np.array([["2000-01-01", "2000-01-02"], ["2000-01-03", "2000-01-04"]]) expected = np.array( [ [DatetimeNoLeap(2000, 1, 1), DatetimeNoLeap(2000, 1, 2)], @@ -1123,6 +1088,5 @@ def test_asi8_distant_date(): date_type = cftime.DatetimeProlepticGregorian index = xr.CFTimeIndex([date_type(10731, 4, 22, 3, 25, 45, 123456)]) result = index.asi8 - expected = np.array( - [1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) + expected = np.array([1000000 * 86400 * 400 * 8000 + 12345 * 1000000 + 123456]) np.testing.assert_array_equal(result, expected) From a9c048f89bbb82f9d7e153c8ee5e3b7756da8148 Mon Sep 17 00:00:00 2001 From: AS Date: Fri, 12 Jun 2020 11:10:44 +0200 Subject: [PATCH 20/35] require cftime for added test --- xarray/tests/test_cftimeindex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index c6e21738329..8f270c415ff 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -920,6 +920,7 @@ def test_cftimeindex_calendar_repr(calendar, expected): assert f" calendar='{expected}'" in repr_str +@requires_cftime @pytest.mark.parametrize("periods", [2, 4]) def test_cftimeindex_periods_repr(periods): """Test that cftimeindex has periods property in repr.""" From fcb48fd041686d9109b9f70c9b9856030dbabf79 Mon Sep 17 00:00:00 2001 From: AS Date: Sun, 14 Jun 2020 18:49:28 +0200 Subject: [PATCH 21/35] implement format_array_flat repr without commata and multiple lines --- xarray/coding/cftimeindex.py | 24 +++++++----------------- xarray/tests/test_cftimeindex.py | 2 +- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 4f138965440..9423b537280 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -50,6 +50,8 @@ from xarray.core.utils import is_scalar from ..core.common import _contains_cftime_datetimes +from ..core.formatting import format_array_flat +from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name @@ -267,23 +269,11 @@ def __repr__(self): expect for attrs.append(("calendar", self.calendar)) """ klass_name = type(self).__name__ - data = self._format_data() - attrs = self._format_attrs() - # add length to attrs - attrs.append(("length", f"'{len(self)}'")) - # add calendar to attrs - attrs.append(("calendar", f"'{self.calendar}'")) - space = self._format_space() - attrs_str = [f"{k}={v}" for k, v in attrs] - prepr = f",{space}".join(attrs_str) - - # no data provided, just attributes - if data is None: - data = "" - - res = f"{klass_name}({data}{prepr})" - - return res + datastr = format_array_flat(self.values, OPTIONS["display_width"]) + attrs = {"length": f"{len(self)}", "calendar": f"'{self.calendar}'"} + attrs_str = [f"{k}={v}" for k, v in attrs.items()] + prepr = f",{' '}".join(attrs_str) + return f"{klass_name}([{datastr}], {prepr})" def _partial_date_slice(self, resolution, parsed): """Adapted from diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 8f270c415ff..2a5cbd8c3c2 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -926,7 +926,7 @@ def test_cftimeindex_periods_repr(periods): """Test that cftimeindex has periods property in repr.""" index = xr.cftime_range(start="2000", periods=periods) repr_str = index.__repr__() - assert f" length='{periods}'" in repr_str + assert f" length={periods}" in repr_str @requires_cftime From 83839ecb7d7d23d3194cb72aed6b3ab4add9b611 Mon Sep 17 00:00:00 2001 From: AS Date: Mon, 15 Jun 2020 11:12:11 +0200 Subject: [PATCH 22/35] reproduce pd.Index repr for CFTimeIndex repr --- xarray/coding/cftimeindex.py | 56 +++++++++++++++++++++++++++-- xarray/tests/test_cftimeindex.py | 62 +++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 9423b537280..58e50029370 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -269,11 +269,61 @@ def __repr__(self): expect for attrs.append(("calendar", self.calendar)) """ klass_name = type(self).__name__ - datastr = format_array_flat(self.values, OPTIONS["display_width"]) - attrs = {"length": f"{len(self)}", "calendar": f"'{self.calendar}'"} + len_item = 19 # length of one item in repr + # shorten repr for more than 100 items + max_width = (19 + 1) * 100 if len(self) <= 100 else 22 * len_item + datastr = format_array_flat(self.values, max_width) + + def join_every_second(s, sep=" ", join=", "): + # to formatting.py + """Join every second item after split(sep).""" + ss = s.split(sep) + sj = [x + " " + y for x, y in zip(ss[0::2], ss[1::2])] + return join.join(sj) + + linebreak_spaces = " " * len(klass_name) + linebreak_add = linebreak_spaces + " " + + def insert_linebreak_after_three(s, sep=",", linebreak=" "): + """Linebreak after three items split(sep).""" + s_sep = s.split(sep) + for i in range(len(s_sep)): + if i % 3 == 0 and i != 0: + s_sep[i] = f"\n{linebreak}{s_sep[i]}" + return sep.join(s_sep) + + if datastr: + if len(self) <= 3: + datastr = join_every_second(datastr) + else: + sepstr = "..." + if sepstr in datastr: + firststr, laststr = datastr.split(f" {sepstr} ") + firststr = insert_linebreak_after_three( + join_every_second(firststr, linebreak=linebreak_add) + ) + laststr = insert_linebreak_after_three( + join_every_second(laststr, linebreak=linebreak_add) + ) + datastr = f"{firststr},\n{linebreak_spaces} {sepstr}\n{linebreak_spaces} {laststr}" + else: + datastr = join_every_second(datastr) + datastr = insert_linebreak_after_three( + datastr, linebreak=linebreak_add + ) + # datastr = insert_linebreak_after_three(datastr) + + attrs = { + "dtype": f"'{self.dtype}'", + "length": f"{len(self)}", + "calendar": f"'{self.calendar}'", + } attrs_str = [f"{k}={v}" for k, v in attrs.items()] prepr = f",{' '}".join(attrs_str) - return f"{klass_name}([{datastr}], {prepr})" + if len(self) <= 3: + return f"{klass_name}([{datastr}], {prepr})" + else: + return f"{klass_name}([{datastr}],\n{linebreak_spaces} {prepr})" def _partial_date_slice(self, resolution, parsed): """Adapted from diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 2a5cbd8c3c2..7622cd5b237 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -918,10 +918,11 @@ def test_cftimeindex_calendar_repr(calendar, expected): index = xr.cftime_range(start="2000", periods=3, calendar=calendar) repr_str = index.__repr__() assert f" calendar='{expected}'" in repr_str + assert "2000-01-01 00:00:00, 2000-01-02 00:00:00" in repr_str @requires_cftime -@pytest.mark.parametrize("periods", [2, 4]) +@pytest.mark.parametrize("periods", [2, 40]) def test_cftimeindex_periods_repr(periods): """Test that cftimeindex has periods property in repr.""" index = xr.cftime_range(start="2000", periods=periods) @@ -929,6 +930,65 @@ def test_cftimeindex_periods_repr(periods): assert f" length={periods}" in repr_str +@requires_cftime +@pytest.mark.parametrize("periods", [2, 3, 4, 100, 101]) +def test_cftimeindex_repr_formatting(periods): + """Test that cftimeindex.__repr__ is formatted as pd.Index.__repr__.""" + index = xr.cftime_range(start="2000", periods=periods) + repr_str = index.__repr__() + print(repr_str) + # check for commata + assert "2000-01-01 00:00:00, 2000-01-02 00:00:00" in repr_str + if periods <= 3: + assert "\n" not in repr_str + "CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00], dtype='object', calendar='standard')" == repr_str + else: + # check for linebreak + assert ", 2000-01-03 00:00:00,\n" in repr_str + # check for times have same indent + lines = repr_str.split("\n") + firststr = "2000" + assert lines[0].find(firststr) == lines[1].find(firststr) + # check for attrs line has one less indent than times + assert lines[-1].find("dtype") + 1 == lines[0].find(firststr) + # check for ... separation dots + if periods > 100: + assert "..." in repr_str + + +@requires_cftime +@pytest.mark.parametrize("periods", [22, 50, 100]) +def test_cftimeindex_repr_101_shorter(periods): + index_101 = xr.cftime_range(start="2000", periods=101) + index_periods = xr.cftime_range(start="2000", periods=periods) + index_101_repr_str = index_101.__repr__() + index_periods_repr_str = index_periods.__repr__() + assert len(index_101_repr_str) < len(index_periods_repr_str) + + +@requires_cftime +@pytest.mark.parametrize("periods", [3, 4, 7, 22, 50, 100, 101, 500]) +def test_cftimeindex_repr_compare_pandasIndex(periods): + cfindex = xr.cftime_range(start="2000", periods=periods) + pdindex = pd.Index(cfindex) + cfindex_repr_str = cfindex.__repr__() + pdindex_repr_str = pdindex.__repr__() + pdindex_repr_str = pdindex_repr_str.replace("Index", "CFTimeIndex") + pdindex_repr_str = pdindex_repr_str.replace(f"\n{' '*7}", f"\n{' '*13}") + if periods > 3: + pdindex_repr_str = pdindex_repr_str.replace("dtype", f"{' '*6}dtype") + if periods <= 100: + lengthstr = f"length={periods}, " + else: + lengthstr = "" + pdindex_repr_str = pdindex_repr_str.replace( + ")", f", {lengthstr}calendar='gregorian')" + ) + print(pdindex_repr_str) + print(cfindex_repr_str) + assert pdindex_repr_str in cfindex_repr_str + + @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap From e3c8c011a23f0c647a1a236a396e0e992abcad8d Mon Sep 17 00:00:00 2001 From: AS Date: Mon, 15 Jun 2020 11:17:32 +0200 Subject: [PATCH 23/35] reproduce pd.Index repr for CFTimeIndex repr --- xarray/coding/cftimeindex.py | 11 +++-------- xarray/tests/test_cftimeindex.py | 6 ++---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 58e50029370..5b6c18ef866 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -51,7 +51,6 @@ from ..core.common import _contains_cftime_datetimes from ..core.formatting import format_array_flat -from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name @@ -300,18 +299,14 @@ def insert_linebreak_after_three(s, sep=",", linebreak=" "): if sepstr in datastr: firststr, laststr = datastr.split(f" {sepstr} ") firststr = insert_linebreak_after_three( - join_every_second(firststr, linebreak=linebreak_add) - ) + join_every_second(firststr), linebreak=linebreak_add) laststr = insert_linebreak_after_three( - join_every_second(laststr, linebreak=linebreak_add) - ) + join_every_second(laststr), linebreak=linebreak_add) datastr = f"{firststr},\n{linebreak_spaces} {sepstr}\n{linebreak_spaces} {laststr}" else: - datastr = join_every_second(datastr) datastr = insert_linebreak_after_three( - datastr, linebreak=linebreak_add + join_every_second(datastr), linebreak=linebreak_add ) - # datastr = insert_linebreak_after_three(datastr) attrs = { "dtype": f"'{self.dtype}'", diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 7622cd5b237..0424adf3a5f 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -967,7 +967,7 @@ def test_cftimeindex_repr_101_shorter(periods): @requires_cftime -@pytest.mark.parametrize("periods", [3, 4, 7, 22, 50, 100, 101, 500]) +@pytest.mark.parametrize("periods", [3, 4, 100, 101]) def test_cftimeindex_repr_compare_pandasIndex(periods): cfindex = xr.cftime_range(start="2000", periods=periods) pdindex = pd.Index(cfindex) @@ -984,9 +984,7 @@ def test_cftimeindex_repr_compare_pandasIndex(periods): pdindex_repr_str = pdindex_repr_str.replace( ")", f", {lengthstr}calendar='gregorian')" ) - print(pdindex_repr_str) - print(cfindex_repr_str) - assert pdindex_repr_str in cfindex_repr_str + assert pdindex_repr_str == cfindex_repr_str @requires_cftime From 69d000fad2afbcee3fc4dd451f7e105e05caa44d Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 7 Jul 2020 11:58:38 +0200 Subject: [PATCH 24/35] sensitive to display_width --- xarray/coding/cftimeindex.py | 48 ++++++++++++++++++++++---------- xarray/tests/test_cftimeindex.py | 39 ++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 5b6c18ef866..a1efb51f266 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -51,6 +51,7 @@ from ..core.common import _contains_cftime_datetimes from ..core.formatting import format_array_flat +from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name @@ -268,13 +269,15 @@ def __repr__(self): expect for attrs.append(("calendar", self.calendar)) """ klass_name = type(self).__name__ - len_item = 19 # length of one item in repr + len_item = 19 # length of one item assuming 4 digit year + display_width = OPTIONS["display_width"] + # separate by newline after sep_after items + sep_after = (display_width - len("CFTimeIndex([")) // len_item # shorten repr for more than 100 items max_width = (19 + 1) * 100 if len(self) <= 100 else 22 * len_item datastr = format_array_flat(self.values, max_width) def join_every_second(s, sep=" ", join=", "): - # to formatting.py """Join every second item after split(sep).""" ss = s.split(sep) sj = [x + " " + y for x, y in zip(ss[0::2], ss[1::2])] @@ -283,28 +286,35 @@ def join_every_second(s, sep=" ", join=", "): linebreak_spaces = " " * len(klass_name) linebreak_add = linebreak_spaces + " " - def insert_linebreak_after_three(s, sep=",", linebreak=" "): - """Linebreak after three items split(sep).""" + def insert_linebreak_after_x_items( + s, sep_after=sep_after, sep=",", linebreak=" " + ): + """Linebreak after `sep_after` items split(sep).""" s_sep = s.split(sep) for i in range(len(s_sep)): - if i % 3 == 0 and i != 0: + if i % sep_after == 0 and i != 0: s_sep[i] = f"\n{linebreak}{s_sep[i]}" return sep.join(s_sep) if datastr: - if len(self) <= 3: + if len(self) <= sep_after: + # timeitems as oneliner datastr = join_every_second(datastr) else: + # linebreak after sep_after time items sepstr = "..." if sepstr in datastr: + # separate upper and lower time items when '...' truncatation firststr, laststr = datastr.split(f" {sepstr} ") - firststr = insert_linebreak_after_three( - join_every_second(firststr), linebreak=linebreak_add) - laststr = insert_linebreak_after_three( - join_every_second(laststr), linebreak=linebreak_add) + firststr = insert_linebreak_after_x_items( + join_every_second(firststr), linebreak=linebreak_add + ) + laststr = insert_linebreak_after_x_items( + join_every_second(laststr), linebreak=linebreak_add + ) datastr = f"{firststr},\n{linebreak_spaces} {sepstr}\n{linebreak_spaces} {laststr}" else: - datastr = insert_linebreak_after_three( + datastr = insert_linebreak_after_x_items( join_every_second(datastr), linebreak=linebreak_add ) @@ -314,11 +324,19 @@ def insert_linebreak_after_three(s, sep=",", linebreak=" "): "calendar": f"'{self.calendar}'", } attrs_str = [f"{k}={v}" for k, v in attrs.items()] - prepr = f",{' '}".join(attrs_str) - if len(self) <= 3: - return f"{klass_name}([{datastr}], {prepr})" + attrs_str = f",{' '}".join(attrs_str) + # oneliner only if smaller than display_width + full_repr_str = f"{klass_name}([{datastr}], {attrs_str})" + if len(self) <= sep_after and len(full_repr_str) <= display_width: + return full_repr_str else: - return f"{klass_name}([{datastr}],\n{linebreak_spaces} {prepr})" + # if attrs_str too long, one per line + if len(attrs_str) >= display_width - len(linebreak_spaces): + attrs_str = attrs_str.replace(",", f",\n{linebreak_spaces}") + full_repr_str = ( + f"{klass_name}([{datastr}],\n{linebreak_spaces} {attrs_str})" + ) + return full_repr_str def _partial_date_slice(self, resolution, parsed): """Adapted from diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 0424adf3a5f..6667f2b447a 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -13,6 +13,7 @@ assert_all_valid_date_type, parse_iso8601, ) +from xarray.core.options import OPTIONS from xarray.tests import assert_array_equal, assert_identical from . import raises_regex, requires_cftime, requires_cftime_1_1_0 @@ -936,15 +937,15 @@ def test_cftimeindex_repr_formatting(periods): """Test that cftimeindex.__repr__ is formatted as pd.Index.__repr__.""" index = xr.cftime_range(start="2000", periods=periods) repr_str = index.__repr__() - print(repr_str) # check for commata assert "2000-01-01 00:00:00, 2000-01-02 00:00:00" in repr_str - if periods <= 3: + # check oneline repr + if len(repr_str) <= OPTIONS["display_width"]: assert "\n" not in repr_str - "CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00], dtype='object', calendar='standard')" == repr_str + # if time items in first line only + elif periods * 19 < OPTIONS["display_width"]: + assert "\n" in repr_str else: - # check for linebreak - assert ", 2000-01-03 00:00:00,\n" in repr_str # check for times have same indent lines = repr_str.split("\n") firststr = "2000" @@ -956,6 +957,24 @@ def test_cftimeindex_repr_formatting(periods): assert "..." in repr_str +@requires_cftime +@pytest.mark.parametrize("display_width", [40, 80, 100]) +@pytest.mark.parametrize("periods", [2, 3, 4, 100, 101]) +def test_cftimeindex_repr_formatting_width(periods, display_width): + """Test that cftimeindex is sensitive to OPTIONS['display_width'].""" + index = xr.cftime_range(start="2000", periods=periods) + len_intro_str = len("CFTimeIndex(") + with xr.set_options(display_width=display_width): + repr_str = index.__repr__() + splitted = repr_str.split("\n") + for i, s in enumerate(splitted): + # check that lines not longer than OPTIONS['display_width'] + assert len(s) <= display_width, f"{len(s)} {s} {display_width}" + if i > 0: + # check for initial spaces + assert s[:len_intro_str] == " " * len_intro_str + + @requires_cftime @pytest.mark.parametrize("periods", [22, 50, 100]) def test_cftimeindex_repr_101_shorter(periods): @@ -969,14 +988,20 @@ def test_cftimeindex_repr_101_shorter(periods): @requires_cftime @pytest.mark.parametrize("periods", [3, 4, 100, 101]) def test_cftimeindex_repr_compare_pandasIndex(periods): + """Test xr.cftimeindex.__repr__ against previous pandas.Index.__repr__. Small adjustments to similarize visuals like indent.""" cfindex = xr.cftime_range(start="2000", periods=periods) pdindex = pd.Index(cfindex) cfindex_repr_str = cfindex.__repr__() pdindex_repr_str = pdindex.__repr__() pdindex_repr_str = pdindex_repr_str.replace("Index", "CFTimeIndex") pdindex_repr_str = pdindex_repr_str.replace(f"\n{' '*7}", f"\n{' '*13}") + if periods <= 3: + # pd.Index doesnt worry about display_width + cfindex_repr_str = cfindex_repr_str.replace("\n", "").replace(" " * 12, " ") if periods > 3: + # indent similarly pdindex_repr_str = pdindex_repr_str.replace("dtype", f"{' '*6}dtype") + # add length attribute if many periods if periods <= 100: lengthstr = f"length={periods}, " else: @@ -984,7 +1009,9 @@ def test_cftimeindex_repr_compare_pandasIndex(periods): pdindex_repr_str = pdindex_repr_str.replace( ")", f", {lengthstr}calendar='gregorian')" ) - assert pdindex_repr_str == cfindex_repr_str + assert pdindex_repr_str == cfindex_repr_str, print( + f"pandas: {pdindex_repr_str}\n vs.\ncftime: {cfindex_repr_str}" + ) @requires_cftime From 80ca89197857d789c99ba310f95806398ec53009 Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 7 Jul 2020 16:06:35 +0200 Subject: [PATCH 25/35] rewritte format_cftimeindex_array from template of format_array_flat --- xarray/coding/cftimeindex.py | 57 +++-------------------- xarray/core/formatting.py | 80 ++++++++++++++++++++++++++++++++ xarray/tests/test_cftimeindex.py | 2 +- 3 files changed, 87 insertions(+), 52 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index a1efb51f266..5267faa8b0b 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -50,7 +50,7 @@ from xarray.core.utils import is_scalar from ..core.common import _contains_cftime_datetimes -from ..core.formatting import format_array_flat +from ..core.formatting import format_cftimeindex_array from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name @@ -264,60 +264,15 @@ def __new__(cls, data, name=None): def __repr__(self): """ Return a string representation for this object. - - copied from pandas.io.printing.py - expect for attrs.append(("calendar", self.calendar)) """ klass_name = type(self).__name__ - len_item = 19 # length of one item assuming 4 digit year display_width = OPTIONS["display_width"] - # separate by newline after sep_after items - sep_after = (display_width - len("CFTimeIndex([")) // len_item - # shorten repr for more than 100 items - max_width = (19 + 1) * 100 if len(self) <= 100 else 22 * len_item - datastr = format_array_flat(self.values, max_width) - - def join_every_second(s, sep=" ", join=", "): - """Join every second item after split(sep).""" - ss = s.split(sep) - sj = [x + " " + y for x, y in zip(ss[0::2], ss[1::2])] - return join.join(sj) - + len_item = 19 # length of one item assuming 4 digit year + sep_after = display_width // (len_item + 1) linebreak_spaces = " " * len(klass_name) - linebreak_add = linebreak_spaces + " " - - def insert_linebreak_after_x_items( - s, sep_after=sep_after, sep=",", linebreak=" " - ): - """Linebreak after `sep_after` items split(sep).""" - s_sep = s.split(sep) - for i in range(len(s_sep)): - if i % sep_after == 0 and i != 0: - s_sep[i] = f"\n{linebreak}{s_sep[i]}" - return sep.join(s_sep) - - if datastr: - if len(self) <= sep_after: - # timeitems as oneliner - datastr = join_every_second(datastr) - else: - # linebreak after sep_after time items - sepstr = "..." - if sepstr in datastr: - # separate upper and lower time items when '...' truncatation - firststr, laststr = datastr.split(f" {sepstr} ") - firststr = insert_linebreak_after_x_items( - join_every_second(firststr), linebreak=linebreak_add - ) - laststr = insert_linebreak_after_x_items( - join_every_second(laststr), linebreak=linebreak_add - ) - datastr = f"{firststr},\n{linebreak_spaces} {sepstr}\n{linebreak_spaces} {laststr}" - else: - datastr = insert_linebreak_after_x_items( - join_every_second(datastr), linebreak=linebreak_add - ) - + datastr = format_cftimeindex_array( + self.values, display_width - len(klass_name), linebreak_spaces + 2 + ) attrs = { "dtype": f"'{self.dtype}'", "length": f"{len(self)}", diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index d6732fc182e..9d2e283af87 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -216,6 +216,86 @@ def format_array_flat(array, max_width: int): return pprint_str +def format_cftimeindex_array(array, display_width, spaces): + """Return a formatted string for as many items in the flattened version of + array that will fit within display_width characters. + """ + # largely copied from xr.core.formatting.format_array_flat + + # every item will take up at least two characters, but we always want to + # print at least first and last items + len_item = 19 # length of one item assuming 4 digit year + sep_after = display_width // (len_item + 1) + # use ellipsis ... for more than 100 items shows 10 front and back items + max_width = (len_item + 1) * 100 if len(array) <= 100 else 22 * len_item + max_possibly_relevant = min( + max(array.size, 1), max(int(np.ceil(max_width / 2.0)), 2) + ) + relevant_front_items = format_items( + first_n_items(array, (max_possibly_relevant + 1) // 2) + ) + relevant_back_items = format_items(last_n_items(array, max_possibly_relevant // 2)) + # interleave relevant front and back items: + # [a, b, c] and [y, z] -> [a, z, b, y, c] + relevant_items = sum( + zip_longest(relevant_front_items, reversed(relevant_back_items)), () + )[:max_possibly_relevant] + + cum_len = np.cumsum([len(s) + 1 for s in relevant_items]) - 1 + + if (array.size > 2) and ( + (max_possibly_relevant < array.size) or (cum_len > max_width).any() + ): + padding = " ... " + count = min( + array.size, max(np.argmax(cum_len + len(padding) - 1 > max_width), 2) + ) + else: + count = array.size + padding = "" if (count <= 1) else ", " + + def insert_linebreak_after_x_items(s, sep_after=sep_after, sep=",", spaces=spaces): + """Linebreak after `sep_after` items from split(sep).""" + s_sep = s.split(sep) + for i in range(len(s_sep)): + if i % sep_after == 0 and i != 0: + s_sep[i] = f"\n{' '*spaces}{s_sep[i]}" + return sep.join(s_sep).replace("\n ", "\n") + + num_front = (count + 1) // 2 + num_back = count - num_front + # note that num_back is 0 <--> array.size is 0 or 1 + # <--> relevant_back_items is [] + if padding == " ... ": + padding = f"\n{' '*(spaces)}{padding[1:-1]}\n" + + if "..." in padding: + pprint_str = "".join( + [ + insert_linebreak_after_x_items( + ", ".join(relevant_front_items[:num_front]) + ) + + ",", + padding, + " " * spaces + + insert_linebreak_after_x_items( + ", ".join(relevant_back_items[-num_back:]) + ), + ] + ) + else: + pprint_str = insert_linebreak_after_x_items( + "".join( + [ + ", ".join(relevant_front_items[:num_front]), + padding, + ", ".join(relevant_back_items[-num_back:]), + ] + ) + ) + return pprint_str + + _KNOWN_TYPE_REPRS = {np.ndarray: "np.ndarray"} with contextlib.suppress(ImportError): import sparse diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 6667f2b447a..9af47d25b76 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -1010,7 +1010,7 @@ def test_cftimeindex_repr_compare_pandasIndex(periods): ")", f", {lengthstr}calendar='gregorian')" ) assert pdindex_repr_str == cfindex_repr_str, print( - f"pandas: {pdindex_repr_str}\n vs.\ncftime: {cfindex_repr_str}" + f"pandas:\n{pdindex_repr_str}\n vs.\ncftime: \n{cfindex_repr_str}" ) From 3080d81d9d4324b87382fcd0031d97c46d1bb5e8 Mon Sep 17 00:00:00 2001 From: AS Date: Tue, 7 Jul 2020 16:26:06 +0200 Subject: [PATCH 26/35] bugfix --- doc/whats-new.rst | 5 +++-- xarray/coding/cftimeindex.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 378d2ccac1e..f403a3dfc87 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -82,8 +82,9 @@ New Features :py:func:`xarray.decode_cf`) that allows to disable/enable the decoding of timedeltas independently of time decoding (:issue:`1621`) `Aureliana Barghini ` -- Add ``calendar`` as a new property for ``CFTimeIndex`` and show in ``calendar`` and - ``length`` in ``CFTimeIndex.__repr__`` (:issue:`2416`, :pull:`4092`) +- Build CFTimeIndex.__repr__ explicitly as pandas.Index. Add ``calendar`` as a new + property for ``CFTimeIndex`` and show in ``calendar`` and ``length`` in + ``CFTimeIndex.__repr__`` (:issue:`2416`, :pull:`4092`) `Aaron Spring ` Bug fixes diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 5267faa8b0b..ad361a6967c 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -269,9 +269,10 @@ def __repr__(self): display_width = OPTIONS["display_width"] len_item = 19 # length of one item assuming 4 digit year sep_after = display_width // (len_item + 1) - linebreak_spaces = " " * len(klass_name) + linebreak_nspaces = len(klass_name) + linebreak_spaces = " " * linebreak_nspaces datastr = format_cftimeindex_array( - self.values, display_width - len(klass_name), linebreak_spaces + 2 + self.values, display_width - len(klass_name), linebreak_nspaces + 2 ) attrs = { "dtype": f"'{self.dtype}'", From b77a2ee3fb8f12fdfb147e52f7f7bfaa3cce48ff Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 15 Jul 2020 12:57:44 +0200 Subject: [PATCH 27/35] new approach --- xarray/coding/cftimeindex.py | 43 ++++++++++----- xarray/core/formatting.py | 101 +++++++++-------------------------- 2 files changed, 54 insertions(+), 90 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index ad361a6967c..9ac475e5f7a 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -50,7 +50,7 @@ from xarray.core.utils import is_scalar from ..core.common import _contains_cftime_datetimes -from ..core.formatting import format_cftimeindex_array +from ..core.formatting import format_times from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name @@ -267,13 +267,30 @@ def __repr__(self): """ klass_name = type(self).__name__ display_width = OPTIONS["display_width"] - len_item = 19 # length of one item assuming 4 digit year - sep_after = display_width // (len_item + 1) - linebreak_nspaces = len(klass_name) - linebreak_spaces = " " * linebreak_nspaces - datastr = format_cftimeindex_array( - self.values, display_width - len(klass_name), linebreak_nspaces + 2 - ) + offset = len(klass_name) + 2 + ITEMS_IN_REPR_MAX = 100 + + if len(self) <= ITEMS_IN_REPR_MAX: + datastr = format_times( + self.values, display_width, offset=offset, first_row_offset=0 + ) + else: + SHOW_ITEMS_FRONT_END = 10 + front_str = format_times( + self.values[:SHOW_ITEMS_FRONT_END], + display_width, + offset=offset, + first_row_offset=0, + last_row_end=",", + ) + end_str = format_times( + self.values[-SHOW_ITEMS_FRONT_END:], + display_width, + offset=offset, + first_row_offset=offset, + ) + datastr = "\n".join([front_str, f"{' '*offset}...", end_str]) + attrs = { "dtype": f"'{self.dtype}'", "length": f"{len(self)}", @@ -283,15 +300,13 @@ def __repr__(self): attrs_str = f",{' '}".join(attrs_str) # oneliner only if smaller than display_width full_repr_str = f"{klass_name}([{datastr}], {attrs_str})" - if len(self) <= sep_after and len(full_repr_str) <= display_width: + if len(full_repr_str) <= display_width: return full_repr_str else: # if attrs_str too long, one per line - if len(attrs_str) >= display_width - len(linebreak_spaces): - attrs_str = attrs_str.replace(",", f",\n{linebreak_spaces}") - full_repr_str = ( - f"{klass_name}([{datastr}],\n{linebreak_spaces} {attrs_str})" - ) + if len(attrs_str) >= display_width - offset: + attrs_str = attrs_str.replace(",", f",\n{' '*(offset-1)}") + full_repr_str = f"{klass_name}([{datastr}],\n{' '*(offset-1)}{attrs_str})" return full_repr_str def _partial_date_slice(self, resolution, parsed): diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 9d2e283af87..a113491d0ae 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -216,84 +216,33 @@ def format_array_flat(array, max_width: int): return pprint_str -def format_cftimeindex_array(array, display_width, spaces): - """Return a formatted string for as many items in the flattened version of - array that will fit within display_width characters. - """ - # largely copied from xr.core.formatting.format_array_flat - - # every item will take up at least two characters, but we always want to - # print at least first and last items - len_item = 19 # length of one item assuming 4 digit year - sep_after = display_width // (len_item + 1) - # use ellipsis ... for more than 100 items shows 10 front and back items - max_width = (len_item + 1) * 100 if len(array) <= 100 else 22 * len_item - max_possibly_relevant = min( - max(array.size, 1), max(int(np.ceil(max_width / 2.0)), 2) - ) - relevant_front_items = format_items( - first_n_items(array, (max_possibly_relevant + 1) // 2) - ) - relevant_back_items = format_items(last_n_items(array, max_possibly_relevant // 2)) - # interleave relevant front and back items: - # [a, b, c] and [y, z] -> [a, z, b, y, c] - relevant_items = sum( - zip_longest(relevant_front_items, reversed(relevant_back_items)), () - )[:max_possibly_relevant] - - cum_len = np.cumsum([len(s) + 1 for s in relevant_items]) - 1 - - if (array.size > 2) and ( - (max_possibly_relevant < array.size) or (cum_len > max_width).any() - ): - padding = " ... " - count = min( - array.size, max(np.argmax(cum_len + len(padding) - 1 > max_width), 2) +def format_row(times, indent=0, separator=", ", row_end=",\n"): + return indent * " " + separator.join(map(str, times)) + row_end + + +def format_times( + index, + max_width, + offset, + separator=", ", + first_row_offset=0, + intermediate_row_end=",\n", + last_row_end="", +): + CFTIME_REPR_LENGTH = 19 + n_per_row = max(max_width // (CFTIME_REPR_LENGTH + len(separator)), 1) + n_rows = int(np.ceil(len(index) / n_per_row)) + + representation = "" + for row in range(n_rows): + indent = first_row_offset if row == 0 else offset + row_end = last_row_end if row == n_rows - 1 else intermediate_row_end + times_for_row = index[row * n_per_row : (row + 1) * n_per_row] + representation = representation + format_row( + times_for_row, indent=indent, separator=separator, row_end=row_end ) - else: - count = array.size - padding = "" if (count <= 1) else ", " - - def insert_linebreak_after_x_items(s, sep_after=sep_after, sep=",", spaces=spaces): - """Linebreak after `sep_after` items from split(sep).""" - s_sep = s.split(sep) - for i in range(len(s_sep)): - if i % sep_after == 0 and i != 0: - s_sep[i] = f"\n{' '*spaces}{s_sep[i]}" - return sep.join(s_sep).replace("\n ", "\n") - num_front = (count + 1) // 2 - num_back = count - num_front - # note that num_back is 0 <--> array.size is 0 or 1 - # <--> relevant_back_items is [] - if padding == " ... ": - padding = f"\n{' '*(spaces)}{padding[1:-1]}\n" - - if "..." in padding: - pprint_str = "".join( - [ - insert_linebreak_after_x_items( - ", ".join(relevant_front_items[:num_front]) - ) - + ",", - padding, - " " * spaces - + insert_linebreak_after_x_items( - ", ".join(relevant_back_items[-num_back:]) - ), - ] - ) - else: - pprint_str = insert_linebreak_after_x_items( - "".join( - [ - ", ".join(relevant_front_items[:num_front]), - padding, - ", ".join(relevant_back_items[-num_back:]), - ] - ) - ) - return pprint_str + return representation _KNOWN_TYPE_REPRS = {np.ndarray: "np.ndarray"} From d8d54ce1e05e5873de480b1592200530a257febf Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 15 Jul 2020 13:02:12 +0200 Subject: [PATCH 28/35] docstring --- xarray/core/formatting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index a113491d0ae..52c583198b1 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -217,6 +217,7 @@ def format_array_flat(array, max_width: int): def format_row(times, indent=0, separator=", ", row_end=",\n"): + """Format a single row from format_times.""" return indent * " " + separator.join(map(str, times)) + row_end @@ -229,6 +230,7 @@ def format_times( intermediate_row_end=",\n", last_row_end="", ): + """Format values of cftimeindex as pd.Index.""" CFTIME_REPR_LENGTH = 19 n_per_row = max(max_width // (CFTIME_REPR_LENGTH + len(separator)), 1) n_rows = int(np.ceil(len(index) / n_per_row)) From 249ae2444f8c619ac0a86f8677bccd6ee5acff18 Mon Sep 17 00:00:00 2001 From: AS Date: Wed, 15 Jul 2020 14:19:44 +0200 Subject: [PATCH 29/35] attrs spaces fix --- xarray/coding/cftimeindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 8b8827da940..bffc642d770 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -305,7 +305,7 @@ def __repr__(self): else: # if attrs_str too long, one per line if len(attrs_str) >= display_width - offset: - attrs_str = attrs_str.replace(",", f",\n{' '*(offset-1)}") + attrs_str = attrs_str.replace(",", f",\n{' '*(offset-2)}") full_repr_str = f"{klass_name}([{datastr}],\n{' '*(offset-1)}{attrs_str})" return full_repr_str From ecef05af93c769c16bf44128fa2def1e7039528b Mon Sep 17 00:00:00 2001 From: AS Date: Sat, 18 Jul 2020 20:49:00 +0200 Subject: [PATCH 30/35] rm pandas test, refactor format_attrs and repr test dedent --- xarray/coding/cftimeindex.py | 54 ++++++++++++++++--- xarray/core/formatting.py | 31 ----------- xarray/tests/test_cftimeindex.py | 89 +++++++++++++------------------- 3 files changed, 83 insertions(+), 91 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index bffc642d770..896bd61c273 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -50,10 +50,14 @@ from xarray.core.utils import is_scalar from ..core.common import _contains_cftime_datetimes -from ..core.formatting import format_times from ..core.options import OPTIONS from .times import _STANDARD_CALENDARS, cftime_to_nptime, infer_calendar_name +# constants for cftimeindex.repr +CFTIME_REPR_LENGTH = 19 +ITEMS_IN_REPR_MAX_ELSE_ELLIPSIS = 100 +REPR_ELLIPSIS_SHOW_ITEMS_FRONT_END = 10 + def named(name, pattern): return "(?P<" + name + ">" + pattern + ")" @@ -217,6 +221,43 @@ def assert_all_valid_date_type(data): ) +def format_row(times, indent=0, separator=", ", row_end=",\n"): + """Format a single row from format_times.""" + return indent * " " + separator.join(map(str, times)) + row_end + + +def format_times( + index, + max_width, + offset, + separator=", ", + first_row_offset=0, + intermediate_row_end=",\n", + last_row_end="", +): + """Format values of cftimeindex as pd.Index.""" + n_per_row = max(max_width // (CFTIME_REPR_LENGTH + len(separator)), 1) + n_rows = int(np.ceil(len(index) / n_per_row)) + + representation = "" + for row in range(n_rows): + indent = first_row_offset if row == 0 else offset + row_end = last_row_end if row == n_rows - 1 else intermediate_row_end + times_for_row = index[row * n_per_row : (row + 1) * n_per_row] + representation = representation + format_row( + times_for_row, indent=indent, separator=separator, row_end=row_end + ) + + return representation + + +def format_attrs(attrs, separator=", "): + """Format attrs dict.""" + attrs_str = [f"{k}={v}" for k, v in attrs.items()] + attrs_str = f"{separator}".join(attrs_str) + return attrs_str + + class CFTimeIndex(pd.Index): """Custom Index for working with CF calendars and dates @@ -268,23 +309,21 @@ def __repr__(self): klass_name = type(self).__name__ display_width = OPTIONS["display_width"] offset = len(klass_name) + 2 - ITEMS_IN_REPR_MAX = 100 - if len(self) <= ITEMS_IN_REPR_MAX: + if len(self) <= ITEMS_IN_REPR_MAX_ELSE_ELLIPSIS: datastr = format_times( self.values, display_width, offset=offset, first_row_offset=0 ) else: - SHOW_ITEMS_FRONT_END = 10 front_str = format_times( - self.values[:SHOW_ITEMS_FRONT_END], + self.values[:REPR_ELLIPSIS_SHOW_ITEMS_FRONT_END], display_width, offset=offset, first_row_offset=0, last_row_end=",", ) end_str = format_times( - self.values[-SHOW_ITEMS_FRONT_END:], + self.values[-REPR_ELLIPSIS_SHOW_ITEMS_FRONT_END:], display_width, offset=offset, first_row_offset=offset, @@ -296,8 +335,7 @@ def __repr__(self): "length": f"{len(self)}", "calendar": f"'{self.calendar}'", } - attrs_str = [f"{k}={v}" for k, v in attrs.items()] - attrs_str = f",{' '}".join(attrs_str) + attrs_str = format_attrs(attrs) # oneliner only if smaller than display_width full_repr_str = f"{klass_name}([{datastr}], {attrs_str})" if len(full_repr_str) <= display_width: diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py index 53152710808..28eaae5f05b 100644 --- a/xarray/core/formatting.py +++ b/xarray/core/formatting.py @@ -216,37 +216,6 @@ def format_array_flat(array, max_width: int): return pprint_str -def format_row(times, indent=0, separator=", ", row_end=",\n"): - """Format a single row from format_times.""" - return indent * " " + separator.join(map(str, times)) + row_end - - -def format_times( - index, - max_width, - offset, - separator=", ", - first_row_offset=0, - intermediate_row_end=",\n", - last_row_end="", -): - """Format values of cftimeindex as pd.Index.""" - CFTIME_REPR_LENGTH = 19 - n_per_row = max(max_width // (CFTIME_REPR_LENGTH + len(separator)), 1) - n_rows = int(np.ceil(len(index) / n_per_row)) - - representation = "" - for row in range(n_rows): - indent = first_row_offset if row == 0 else offset - row_end = last_row_end if row == n_rows - 1 else intermediate_row_end - times_for_row = index[row * n_per_row : (row + 1) * n_per_row] - representation = representation + format_row( - times_for_row, indent=indent, separator=separator, row_end=row_end - ) - - return representation - - _KNOWN_TYPE_REPRS = {np.ndarray: "np.ndarray"} with contextlib.suppress(ImportError): import sparse diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 81370f4d93e..20d10cc43ef 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -1,4 +1,5 @@ from datetime import timedelta +from textwrap import dedent import numpy as np import pandas as pd @@ -13,7 +14,6 @@ assert_all_valid_date_type, parse_iso8601, ) -from xarray.core.options import OPTIONS from xarray.tests import assert_array_equal, assert_identical from . import raises_regex, requires_cftime, requires_cftime_1_1_0 @@ -932,29 +932,43 @@ def test_cftimeindex_periods_repr(periods): @requires_cftime -@pytest.mark.parametrize("periods", [2, 3, 4, 100, 101]) -def test_cftimeindex_repr_formatting(periods): - """Test that cftimeindex.__repr__ is formatted as pd.Index.__repr__.""" +@pytest.mark.parametrize( + "periods,expected", + [ + ( + 2, + f"""\ +CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00], + dtype='object', length=2, calendar='gregorian')""", + ), + ( + 4, + f"""\ +CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00, + 2000-01-04 00:00:00], + dtype='object', length=4, calendar='gregorian')""", + ), + ( + 101, + f"""\ +CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00, + 2000-01-04 00:00:00, 2000-01-05 00:00:00, 2000-01-06 00:00:00, + 2000-01-07 00:00:00, 2000-01-08 00:00:00, 2000-01-09 00:00:00, + 2000-01-10 00:00:00, + ... + 2000-04-01 00:00:00, 2000-04-02 00:00:00, 2000-04-03 00:00:00, + 2000-04-04 00:00:00, 2000-04-05 00:00:00, 2000-04-06 00:00:00, + 2000-04-07 00:00:00, 2000-04-08 00:00:00, 2000-04-09 00:00:00, + 2000-04-10 00:00:00], + dtype='object', length=101, calendar='gregorian')""", + ), + ], +) +def test_cftimeindex_repr_formatting(periods, expected): + """Test that cftimeindex.__repr__ is formatted similar to pd.Index.__repr__.""" index = xr.cftime_range(start="2000", periods=periods) - repr_str = index.__repr__() - # check for commata - assert "2000-01-01 00:00:00, 2000-01-02 00:00:00" in repr_str - # check oneline repr - if len(repr_str) <= OPTIONS["display_width"]: - assert "\n" not in repr_str - # if time items in first line only - elif periods * 19 < OPTIONS["display_width"]: - assert "\n" in repr_str - else: - # check for times have same indent - lines = repr_str.split("\n") - firststr = "2000" - assert lines[0].find(firststr) == lines[1].find(firststr) - # check for attrs line has one less indent than times - assert lines[-1].find("dtype") + 1 == lines[0].find(firststr) - # check for ... separation dots - if periods > 100: - assert "..." in repr_str + expected = dedent(expected) + assert expected == repr(index) @requires_cftime @@ -985,35 +999,6 @@ def test_cftimeindex_repr_101_shorter(periods): assert len(index_101_repr_str) < len(index_periods_repr_str) -@requires_cftime -@pytest.mark.parametrize("periods", [3, 4, 100, 101]) -def test_cftimeindex_repr_compare_pandasIndex(periods): - """Test xr.cftimeindex.__repr__ against previous pandas.Index.__repr__. Small adjustments to similarize visuals like indent.""" - cfindex = xr.cftime_range(start="2000", periods=periods) - pdindex = pd.Index(cfindex) - cfindex_repr_str = cfindex.__repr__() - pdindex_repr_str = pdindex.__repr__() - pdindex_repr_str = pdindex_repr_str.replace("Index", "CFTimeIndex") - pdindex_repr_str = pdindex_repr_str.replace(f"\n{' '*7}", f"\n{' '*13}") - if periods <= 3: - # pd.Index doesnt worry about display_width - cfindex_repr_str = cfindex_repr_str.replace("\n", "").replace(" " * 12, " ") - if periods > 3: - # indent similarly - pdindex_repr_str = pdindex_repr_str.replace("dtype", f"{' '*6}dtype") - # add length attribute if many periods - if periods <= 100: - lengthstr = f"length={periods}, " - else: - lengthstr = "" - pdindex_repr_str = pdindex_repr_str.replace( - ")", f", {lengthstr}calendar='gregorian')" - ) - assert pdindex_repr_str == cfindex_repr_str, print( - f"pandas:\n{pdindex_repr_str}\n vs.\ncftime: \n{cfindex_repr_str}" - ) - - @requires_cftime def test_parse_array_of_cftime_strings(): from cftime import DatetimeNoLeap From 7c31e3aebbeda6194800f80776ea7fb6bc550ffd Mon Sep 17 00:00:00 2001 From: AS Date: Sat, 18 Jul 2020 21:03:43 +0200 Subject: [PATCH 31/35] rm f lint --- xarray/tests/test_cftimeindex.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index 20d10cc43ef..642609ba059 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -937,20 +937,20 @@ def test_cftimeindex_periods_repr(periods): [ ( 2, - f"""\ + """\ CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00], dtype='object', length=2, calendar='gregorian')""", ), ( 4, - f"""\ + """\ CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00, 2000-01-04 00:00:00], dtype='object', length=4, calendar='gregorian')""", ), ( 101, - f"""\ + """\ CFTimeIndex([2000-01-01 00:00:00, 2000-01-02 00:00:00, 2000-01-03 00:00:00, 2000-01-04 00:00:00, 2000-01-05 00:00:00, 2000-01-06 00:00:00, 2000-01-07 00:00:00, 2000-01-08 00:00:00, 2000-01-09 00:00:00, From 683f00c95809edb79633994f886533cbe69cf54b Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 19 Jul 2020 09:34:44 -0400 Subject: [PATCH 32/35] Pass index to format_attrs instead of attrs dict --- xarray/coding/cftimeindex.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 896bd61c273..824f9a6d7a0 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -251,8 +251,13 @@ def format_times( return representation -def format_attrs(attrs, separator=", "): - """Format attrs dict.""" +def format_attrs(index, separator=", "): + """Format attributes of CFTimeIndex for __repr__.""" + attrs = { + "dtype": f"'{index.dtype}'", + "length": f"{len(index)}", + "calendar": f"'{index.calendar}'", + } attrs_str = [f"{k}={v}" for k, v in attrs.items()] attrs_str = f"{separator}".join(attrs_str) return attrs_str @@ -330,12 +335,7 @@ def __repr__(self): ) datastr = "\n".join([front_str, f"{' '*offset}...", end_str]) - attrs = { - "dtype": f"'{self.dtype}'", - "length": f"{len(self)}", - "calendar": f"'{self.calendar}'", - } - attrs_str = format_attrs(attrs) + attrs_str = format_attrs(self) # oneliner only if smaller than display_width full_repr_str = f"{klass_name}([{datastr}], {attrs_str})" if len(full_repr_str) <= display_width: From 27074094cc0723aaf9b418d0d36691d8e5622ebd Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 19 Jul 2020 09:39:57 -0400 Subject: [PATCH 33/35] Update whats-new.rst --- doc/whats-new.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 01f67fcc1b5..426e11c6d91 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -25,10 +25,10 @@ Breaking changes New Features ~~~~~~~~~~~~ -- Build CFTimeIndex.__repr__ explicitly as pandas.Index. Add ``calendar`` as a new - property for ``CFTimeIndex`` and show in ``calendar`` and ``length`` in - ``CFTimeIndex.__repr__`` (:issue:`2416`, :pull:`4092`) - `Aaron Spring ` +- Build :py:meth:`CFTimeIndex.__repr__` explicitly as :py:class:`pandas.Index`. Add ``calendar`` as a new + property for :py:class:`CFTimeIndex` and show ``calendar`` and ``length`` in + :py:meth:`CFTimeIndex.__repr__` (:issue:`2416`, :pull:`4092`) + `Aaron Spring `. Bug fixes From b7552b34f063de7ce4afe6bba38361c09f5cabd4 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 19 Jul 2020 09:41:31 -0400 Subject: [PATCH 34/35] Add docstring for new calendar property --- xarray/coding/cftimeindex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index 824f9a6d7a0..cd57af5c7eb 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -672,6 +672,7 @@ def asi8(self): @property def calendar(self): + """The calendar used by the datetimes in the index.""" from .times import infer_calendar_name return infer_calendar_name(self) From e8d85db357e393fc2fca3d9cdef4035307d46b8b Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 19 Jul 2020 13:01:15 -0400 Subject: [PATCH 35/35] Update doc/whats-new.rst Co-authored-by: keewis --- doc/whats-new.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 426e11c6d91..e109633a5e1 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -28,7 +28,7 @@ New Features - Build :py:meth:`CFTimeIndex.__repr__` explicitly as :py:class:`pandas.Index`. Add ``calendar`` as a new property for :py:class:`CFTimeIndex` and show ``calendar`` and ``length`` in :py:meth:`CFTimeIndex.__repr__` (:issue:`2416`, :pull:`4092`) - `Aaron Spring `. + `Aaron Spring `_. Bug fixes