From 02aa01e2b01015be1e8ae0f3b06cb7e971e1fa5d Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:11:56 -0500 Subject: [PATCH 1/7] Adapt to CF conventions 1.9 --- HISTORY.rst | 4 ++++ xclim/core/calendar.py | 16 +++++++++++----- xclim/testing/tests/test_calendar.py | 14 +++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7903def43..36319c525 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,10 @@ New features and enhancements * Added an optimized pathway for ``xclim.indices.run_length`` functions when ``window=1``. (:pull:`911`, :issue:`910`). * The data input frequency expected by ``Indicator``s is now in the ``src_freq`` attribute and is thus controlable by subclassing existing indicators. (:issue:`898`, :pull:`927`). +Breaking changes +~~~~~~~~~~~~~~~~ +* Following version 1.9 of the CF Conventions, publised in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). + Internal changes ~~~~~~~~~~~~~~~~ * Removed some logging configurations in ``dataflags`` that were polluting python's main logging configuration. (:pull:`909`). diff --git a/xclim/core/calendar.py b/xclim/core/calendar.py index 0e5354207..6d873ec8f 100644 --- a/xclim/core/calendar.py +++ b/xclim/core/calendar.py @@ -70,19 +70,23 @@ def get_calendar(obj: Any, dim: str = "time") -> str: ------- str The cftime calendar name or "default" when the data is using numpy's or python's datetime types. + Will always return "standard" instead of "gregorian", following CF conventions 1.9. """ if isinstance(obj, (xr.DataArray, xr.Dataset)): if obj[dim].dtype == "O": obj = obj[dim].where(obj[dim].notnull(), drop=True)[0].item() elif "datetime64" in obj[dim].dtype.name: return "default" - - obj = np.take( - obj, 0 - ) # Take zeroth element, overcome cases when arrays or lists are passed. + elif isinstance(obj, xr.CFTimeIndex): + obj = obj.values[0] + else: + obj = np.take(obj, 0) + # Take zeroth element, overcome cases when arrays or lists are passed. if isinstance(obj, pydt.datetime): # Also covers pandas Timestamp return "default" if isinstance(obj, cftime.datetime): + if obj.calendar == "gregorian": + return "standard" return obj.calendar raise ValueError(f"Calendar could not be inferred from object of type {type(obj)}.") @@ -450,7 +454,7 @@ def ensure_cftime_array( ) -> Union[CFTimeIndex, np.ndarray]: """Convert an input 1D array to an array of cftime objects. - Python's datetime are converted to cftime.DatetimeGregorian. + Python's datetime are converted to cftime.DatetimeGregorian ("standard" calendar). Raises ValueError when unable to cast the input. """ @@ -458,6 +462,8 @@ def ensure_cftime_array( time = time.indexes["time"] elif isinstance(time, np.ndarray): time = pd.DatetimeIndex(time) + if isinstance(time, xr.CFTimeIndex): + return time.values if isinstance(time[0], cftime.datetime): return time if isinstance(time[0], pydt.datetime): diff --git a/xclim/testing/tests/test_calendar.py b/xclim/testing/tests/test_calendar.py index fd46282f4..2e324d786 100644 --- a/xclim/testing/tests/test_calendar.py +++ b/xclim/testing/tests/test_calendar.py @@ -331,7 +331,7 @@ def test_convert_calendar_missing(source, target, freq): ("standard", "noleap"), ("noleap", "default"), ("standard", "360_day"), - ("360_day", "gregorian"), + ("360_day", "standard"), ("noleap", "all_leap"), ("360_day", "noleap"), ], @@ -368,20 +368,20 @@ def test_interp_calendar(source, target): dims=("time",), name="time", ), - "gregorian", + "standard", ), - (date_range("2004-01-01", "2004-01-10", freq="D"), "gregorian"), + (date_range("2004-01-01", "2004-01-10", freq="D"), "standard"), ( xr.DataArray(date_range("2004-01-01", "2004-01-10", freq="D")).values, - "gregorian", + "standard", ), - (date_range("2004-01-01", "2004-01-10", freq="D"), "gregorian"), + (date_range("2004-01-01", "2004-01-10", freq="D"), "standard"), (date_range("2004-01-01", "2004-01-10", freq="D", calendar="julian"), "julian"), ], ) def test_ensure_cftime_array(inp, calout): out = ensure_cftime_array(inp) - assert out[0].calendar == calout + assert get_calendar(out) == calout @pytest.mark.parametrize( @@ -391,7 +391,7 @@ def test_ensure_cftime_array(inp, calout): (2004, "noleap", 365), (2004, "all_leap", 366), (1500, "default", 365), - (1500, "gregorian", 366), + (1500, "standard", 366), (1500, "proleptic_gregorian", 365), (2030, "360_day", 360), ], From 581c55caa41daf1d1c15aee2fd7cc0508df62229 Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:16:27 -0500 Subject: [PATCH 2/7] upd hist --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 36319c525..db5dd2a23 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,7 +13,7 @@ New features and enhancements Breaking changes ~~~~~~~~~~~~~~~~ -* Following version 1.9 of the CF Conventions, publised in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). +* Following version 1.9 of the CF Conventions, publised in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). (:pull:`935`). Internal changes ~~~~~~~~~~~~~~~~ From c69605de836cdef68cb072a2eb43b0e4d75a89a1 Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:18:36 -0500 Subject: [PATCH 3/7] Update xclim/core/calendar.py --- xclim/core/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xclim/core/calendar.py b/xclim/core/calendar.py index 6d873ec8f..34959526d 100644 --- a/xclim/core/calendar.py +++ b/xclim/core/calendar.py @@ -451,7 +451,7 @@ def _convert_datetime( def ensure_cftime_array( time: Sequence, -) -> Union[CFTimeIndex, np.ndarray]: +) -> np.ndarray: """Convert an input 1D array to an array of cftime objects. Python's datetime are converted to cftime.DatetimeGregorian ("standard" calendar). From 11e1c5612f5788e384f7ebe1f9020cd43c9c20bd Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:21:54 -0500 Subject: [PATCH 4/7] Update HISTORY.rst Co-authored-by: David Huard --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index db5dd2a23..8337483e3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,7 +13,7 @@ New features and enhancements Breaking changes ~~~~~~~~~~~~~~~~ -* Following version 1.9 of the CF Conventions, publised in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). (:pull:`935`). +* Following version 1.9 of the CF Conventions, published in September 2021, the calendar name "gregorian" is deprecated. ``core.calendar.get_calendar`` will return "standard", even if the underlying cftime objects still use "gregorian" (cftime <= 1.5.1). (:pull:`935`). Internal changes ~~~~~~~~~~~~~~~~ From 8fc8e67058020d8b4796816f542394552f983057 Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:23:16 -0500 Subject: [PATCH 5/7] style of return annotation --- xclim/core/calendar.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xclim/core/calendar.py b/xclim/core/calendar.py index 34959526d..c0bc065c0 100644 --- a/xclim/core/calendar.py +++ b/xclim/core/calendar.py @@ -449,10 +449,8 @@ def _convert_datetime( return np.nan -def ensure_cftime_array( - time: Sequence, -) -> np.ndarray: - """Convert an input 1D array to an array of cftime objects. +def ensure_cftime_array(time: Sequence) -> np.ndarray: + """Convert an input 1D array to a numpy array of cftime objects. Python's datetime are converted to cftime.DatetimeGregorian ("standard" calendar). From 55dfb5934bf43b9f5f282f234d158820c7502bbd Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 16:36:10 -0500 Subject: [PATCH 6/7] Add test to please the coveralls gods --- xclim/testing/tests/test_calendar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xclim/testing/tests/test_calendar.py b/xclim/testing/tests/test_calendar.py index 2e324d786..82389eee3 100644 --- a/xclim/testing/tests/test_calendar.py +++ b/xclim/testing/tests/test_calendar.py @@ -168,6 +168,7 @@ def test_get_calendar(file, cal, maxdoy): (pd.Timestamp.now(), "default"), (cftime.DatetimeAllLeap(2000, 1, 1), "all_leap"), (np.array([cftime.DatetimeNoLeap(2000, 1, 1)]), "noleap"), + (xr.cftime_range("2000-01-01", periods=4, freq="D"), "standard"), ], ) def test_get_calendar_nonxr(obj, cal): From 441cf9a7066b858041dd486c65053a72d7971ebb Mon Sep 17 00:00:00 2001 From: Pascal Bourgault Date: Thu, 25 Nov 2021 17:05:44 -0500 Subject: [PATCH 7/7] Gods were not pleased, try again --- xclim/testing/tests/test_calendar.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/xclim/testing/tests/test_calendar.py b/xclim/testing/tests/test_calendar.py index 82389eee3..10a2f1878 100644 --- a/xclim/testing/tests/test_calendar.py +++ b/xclim/testing/tests/test_calendar.py @@ -175,6 +175,12 @@ def test_get_calendar_nonxr(obj, cal): assert get_calendar(obj) == cal +@pytest.mark.parametrize("obj", ["astring", {"a": "dict"}, lambda x: x]) +def test_get_calendar_errors(obj): + with pytest.raises(ValueError, match="Calendar could not be inferred from object"): + get_calendar(obj) + + @pytest.mark.parametrize( "source,target,target_as_str,freq", [ @@ -376,7 +382,7 @@ def test_interp_calendar(source, target): xr.DataArray(date_range("2004-01-01", "2004-01-10", freq="D")).values, "standard", ), - (date_range("2004-01-01", "2004-01-10", freq="D"), "standard"), + (date_range("2004-01-01", "2004-01-10", freq="D").values, "standard"), (date_range("2004-01-01", "2004-01-10", freq="D", calendar="julian"), "julian"), ], )