From 936299c7a405a4484b3b89b9cc99783e5879f9c5 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Fri, 7 Oct 2022 22:37:36 -0700 Subject: [PATCH 1/4] CLN: tseries/offsets base tests --- pandas/tests/tseries/offsets/common.py | 123 +-------- pandas/tests/tseries/offsets/test_common.py | 254 ++++++++++++++++++ .../offsets/test_custom_business_hour.py | 5 - pandas/tests/tseries/offsets/test_offsets.py | 9 +- 4 files changed, 263 insertions(+), 128 deletions(-) create mode 100644 pandas/tests/tseries/offsets/test_common.py diff --git a/pandas/tests/tseries/offsets/common.py b/pandas/tests/tseries/offsets/common.py index c12130544e6b2..50b6907a02eaf 100644 --- a/pandas/tests/tseries/offsets/common.py +++ b/pandas/tests/tseries/offsets/common.py @@ -5,25 +5,16 @@ from datetime import datetime -from dateutil.tz.tz import tzlocal -import pytest - -from pandas._libs.tslibs import ( - OutOfBoundsDatetime, - Timestamp, -) +from pandas._libs.tslibs import Timestamp from pandas._libs.tslibs.offsets import ( FY5253, BaseOffset, - BusinessHour, - CustomBusinessHour, DateOffset, FY5253Quarter, LastWeekOfMonth, Week, WeekOfMonth, ) -from pandas.compat import IS64 def assert_offset_equal(offset, base, expected): @@ -63,15 +54,6 @@ class Base: _offset: type[BaseOffset] | None = None d = Timestamp(datetime(2008, 1, 2)) - timezones = [ - None, - "UTC", - "Asia/Tokyo", - "US/Eastern", - "dateutil/Asia/Tokyo", - "dateutil/US/Pacific", - ] - def _get_offset(self, klass, value=1, normalize=False): # create instance from offset class if klass is FY5253: @@ -102,106 +84,3 @@ def _get_offset(self, klass, value=1, normalize=False): else: klass = klass(value, normalize=normalize) return klass - - def test_apply_out_of_range(self, request, tz_naive_fixture): - tz = tz_naive_fixture - if self._offset is None: - return - - # try to create an out-of-bounds result timestamp; if we can't create - # the offset skip - try: - if self._offset in (BusinessHour, CustomBusinessHour): - # Using 10000 in BusinessHour fails in tz check because of DST - # difference - offset = self._get_offset(self._offset, value=100000) - else: - offset = self._get_offset(self._offset, value=10000) - - result = Timestamp("20080101") + offset - assert isinstance(result, datetime) - assert result.tzinfo is None - - # Check tz is preserved - t = Timestamp("20080101", tz=tz) - result = t + offset - assert isinstance(result, datetime) - - if isinstance(tz, tzlocal) and not IS64: - # If we hit OutOfBoundsDatetime on non-64 bit machines - # we'll drop out of the try clause before the next test - request.node.add_marker( - pytest.mark.xfail(reason="OverflowError inside tzlocal past 2038") - ) - assert t.tzinfo == result.tzinfo - - except OutOfBoundsDatetime: - pass - except (ValueError, KeyError): - # we are creating an invalid offset - # so ignore - pass - - def test_offsets_compare_equal(self): - # root cause of GH#456: __ne__ was not implemented - if self._offset is None: - return - offset1 = self._offset() - offset2 = self._offset() - assert not offset1 != offset2 - assert offset1 == offset2 - - def test_rsub(self): - if self._offset is None or not hasattr(self, "offset2"): - # i.e. skip for TestCommon and YQM subclasses that do not have - # offset2 attr - return - assert self.d - self.offset2 == (-self.offset2)._apply(self.d) - - def test_radd(self): - if self._offset is None or not hasattr(self, "offset2"): - # i.e. skip for TestCommon and YQM subclasses that do not have - # offset2 attr - return - assert self.d + self.offset2 == self.offset2 + self.d - - def test_sub(self): - if self._offset is None or not hasattr(self, "offset2"): - # i.e. skip for TestCommon and YQM subclasses that do not have - # offset2 attr - return - off = self.offset2 - msg = "Cannot subtract datetime from offset" - with pytest.raises(TypeError, match=msg): - off - self.d - - assert 2 * off - off == off - assert self.d - self.offset2 == self.d + self._offset(-2) - assert self.d - self.offset2 == self.d - (2 * off - off) - - def testMult1(self): - if self._offset is None or not hasattr(self, "offset1"): - # i.e. skip for TestCommon and YQM subclasses that do not have - # offset1 attr - return - assert self.d + 10 * self.offset1 == self.d + self._offset(10) - assert self.d + 5 * self.offset1 == self.d + self._offset(5) - - def testMult2(self): - if self._offset is None: - return - assert self.d + (-5 * self._offset(-10)) == self.d + self._offset(50) - assert self.d + (-3 * self._offset(-2)) == self.d + self._offset(6) - - def test_compare_str(self): - # GH#23524 - # comparing to strings that cannot be cast to DateOffsets should - # not raise for __eq__ or __ne__ - if self._offset is None: - return - off = self._get_offset(self._offset) - - assert not off == "infer" - assert off != "foo" - # Note: inequalities are only implemented for Tick subclasses; - # tests for this are in test_ticks diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py new file mode 100644 index 0000000000000..f886034fe07fa --- /dev/null +++ b/pandas/tests/tseries/offsets/test_common.py @@ -0,0 +1,254 @@ +from datetime import datetime + +from dateutil.tz.tz import tzlocal +import pytest + +from pandas._libs.tslibs import ( + OutOfBoundsDatetime, + Timestamp, +) +from pandas.compat import IS64 + +from pandas.tseries.offsets import ( + FY5253, + BDay, + BMonthBegin, + BMonthEnd, + BQuarterBegin, + BQuarterEnd, + BusinessHour, + BYearBegin, + BYearEnd, + CBMonthBegin, + CBMonthEnd, + CDay, + CustomBusinessHour, + DateOffset, + FY5253Quarter, + LastWeekOfMonth, + MonthBegin, + MonthEnd, + QuarterEnd, + SemiMonthBegin, + SemiMonthEnd, + Week, + WeekOfMonth, + YearBegin, + YearEnd, +) + + +def _get_offset(klass, value=1, normalize=False): + # create instance from offset class + if klass is FY5253: + klass = klass( + n=value, + startingMonth=1, + weekday=1, + variation="last", + normalize=normalize, + ) + elif klass is FY5253Quarter: + klass = klass( + n=value, + startingMonth=1, + weekday=1, + qtr_with_extra_week=1, + variation="last", + normalize=normalize, + ) + elif klass is LastWeekOfMonth: + klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is WeekOfMonth: + klass = klass(n=value, week=1, weekday=5, normalize=normalize) + elif klass is Week: + klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is DateOffset: + klass = klass(days=value, normalize=normalize) + else: + klass = klass(value, normalize=normalize) + return klass + + +@pytest.fixture( + params=[ + BDay, + BusinessHour, + BMonthEnd, + BMonthBegin, + BQuarterEnd, + BQuarterBegin, + BYearEnd, + BYearBegin, + CDay, + CustomBusinessHour, + CBMonthEnd, + CBMonthBegin, + MonthEnd, + MonthBegin, + SemiMonthBegin, + SemiMonthEnd, + QuarterEnd, + LastWeekOfMonth, + WeekOfMonth, + Week, + YearBegin, + YearEnd, + FY5253, + FY5253Quarter, + DateOffset, + ] +) +def _offset(request): + return request.param + + +@pytest.fixture +def d(_offset): + if _offset in (CBMonthBegin, CBMonthEnd, BDay): + return Timestamp(2008, 1, 1) + elif _offset is (CustomBusinessHour, BusinessHour): + return Timestamp(2014, 7, 1, 10, 00) + return Timestamp(2008, 1, 2) + + +def test_apply_out_of_range(request, tz_naive_fixture, _offset): + tz = tz_naive_fixture + + # try to create an out-of-bounds result timestamp; if we can't create + # the offset skip + try: + if _offset in (BusinessHour, CustomBusinessHour): + # Using 10000 in BusinessHour fails in tz check because of DST + # difference + offset = _get_offset(_offset, value=100000) + else: + offset = _get_offset(_offset, value=10000) + + result = Timestamp("20080101") + offset + assert isinstance(result, datetime) + assert result.tzinfo is None + + # Check tz is preserved + t = Timestamp("20080101", tz=tz) + result = t + offset + assert isinstance(result, datetime) + + if isinstance(tz, tzlocal) and not IS64: + # If we hit OutOfBoundsDatetime on non-64 bit machines + # we'll drop out of the try clause before the next test + request.node.add_marker( + pytest.mark.xfail(reason="OverflowError inside tzlocal past 2038") + ) + assert str(t.tzinfo) == str(result.tzinfo) + + except OutOfBoundsDatetime: + pass + except (ValueError, KeyError): + # we are creating an invalid offset + # so ignore + pass + + +def test_offsets_compare_equal(_offset): + # root cause of GH#456: __ne__ was not implemented + offset1 = _offset() + offset2 = _offset() + assert not offset1 != offset2 + assert offset1 == offset2 + + +@pytest.mark.parametrize( + "date, offset2", + [ + [Timestamp(2008, 1, 1), BDay(2)], + [Timestamp(2014, 7, 1, 10, 00), BusinessHour(n=3)], + [ + Timestamp(2014, 7, 1, 10), + CustomBusinessHour( + holidays=["2014-06-27", Timestamp(2014, 6, 30), Timestamp("2014-07-02")] + ), + ], + [Timestamp(2008, 1, 2), SemiMonthEnd(2)], + [Timestamp(2008, 1, 2), SemiMonthBegin(2)], + [Timestamp(2008, 1, 2), Week(2)], + [Timestamp(2008, 1, 2), WeekOfMonth(2)], + [Timestamp(2008, 1, 2), LastWeekOfMonth(2)], + ], +) +def test_rsub(date, offset2): + assert date - offset2 == (-offset2)._apply(date) + + +@pytest.mark.parametrize( + "date, offset2", + [ + [Timestamp(2008, 1, 1), BDay(2)], + [Timestamp(2014, 7, 1, 10, 00), BusinessHour(n=3)], + [ + Timestamp(2014, 7, 1, 10), + CustomBusinessHour( + holidays=["2014-06-27", Timestamp(2014, 6, 30), Timestamp("2014-07-02")] + ), + ], + [Timestamp(2008, 1, 2), SemiMonthEnd(2)], + [Timestamp(2008, 1, 2), SemiMonthBegin(2)], + [Timestamp(2008, 1, 2), Week(2)], + [Timestamp(2008, 1, 2), WeekOfMonth(2)], + [Timestamp(2008, 1, 2), LastWeekOfMonth(2)], + ], +) +def test_radd(date, offset2): + assert date + offset2 == offset2 + date + + +@pytest.mark.parametrize( + "date, offset_box, offset2", + [ + [Timestamp(2008, 1, 1), BDay, BDay(2)], + [Timestamp(2008, 1, 2), SemiMonthEnd, SemiMonthEnd(2)], + [Timestamp(2008, 1, 2), SemiMonthBegin, SemiMonthBegin(2)], + [Timestamp(2008, 1, 2), Week, Week(2)], + [Timestamp(2008, 1, 2), WeekOfMonth, WeekOfMonth(2)], + [Timestamp(2008, 1, 2), LastWeekOfMonth, LastWeekOfMonth(2)], + ], +) +def test_sub(date, offset_box, offset2): + off = offset2 + msg = "Cannot subtract datetime from offset" + with pytest.raises(TypeError, match=msg): + off - date + + assert 2 * off - off == off + assert date - offset2 == date + offset_box(-2) + assert date - offset2 == date - (2 * off - off) + + +@pytest.mark.parametrize( + "offset_box, offset1", + [ + [BDay, BDay()], + [LastWeekOfMonth, LastWeekOfMonth()], + [WeekOfMonth, WeekOfMonth()], + [Week, Week()], + [SemiMonthBegin, SemiMonthBegin()], + [SemiMonthEnd, SemiMonthEnd()], + [CustomBusinessHour, CustomBusinessHour(weekmask="Tue Wed Thu Fri")], + [BusinessHour, BusinessHour()], + ], +) +def test_Mult1(offset_box, offset1, d): + assert d + 10 * offset1 == d + offset_box(10) + assert d + 5 * offset1 == d + offset_box(5) + + +def test_compare_str(_offset): + # GH#23524 + # comparing to strings that cannot be cast to DateOffsets should + # not raise for __eq__ or __ne__ + off = _get_offset(_offset) + + assert not off == "infer" + assert off != "foo" + # Note: inequalities are only implemented for Tick subclasses; + # tests for this are in test_ticks diff --git a/pandas/tests/tseries/offsets/test_custom_business_hour.py b/pandas/tests/tseries/offsets/test_custom_business_hour.py index 9a2d24f1d0307..360b541ce7c90 100644 --- a/pandas/tests/tseries/offsets/test_custom_business_hour.py +++ b/pandas/tests/tseries/offsets/test_custom_business_hour.py @@ -86,11 +86,6 @@ def test_eq(self): holidays=["2014-06-28"] ) - def test_sub(self): - # override the Base.test_sub implementation because self.offset2 is - # defined differently in this class than the test expects - pass - def test_hash(self): assert hash(self.offset1) == hash(self.offset1) assert hash(self.offset2) == hash(self.offset2) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index bca4ba98f37b7..829b378d5b20f 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -233,7 +233,14 @@ def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=Fals # test tz when input is datetime or Timestamp return - for tz in self.timezones: + for tz in [ + None, + "UTC", + "Asia/Tokyo", + "US/Eastern", + "dateutil/Asia/Tokyo", + "dateutil/US/Pacific", + ]: expected_localize = expected.tz_localize(tz) tz_obj = timezones.maybe_get_tz(tz) dt_tz = conversion.localize_pydatetime(dt, tz_obj) From 67dd5d3fd6b6c18df77a4169108c31d771bdb7a1 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 10 Oct 2022 11:06:30 -0700 Subject: [PATCH 2/4] test passing now --- pandas/tests/tseries/offsets/test_common.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py index f886034fe07fa..aa900f54bc8e7 100644 --- a/pandas/tests/tseries/offsets/test_common.py +++ b/pandas/tests/tseries/offsets/test_common.py @@ -1,13 +1,11 @@ from datetime import datetime -from dateutil.tz.tz import tzlocal import pytest from pandas._libs.tslibs import ( OutOfBoundsDatetime, Timestamp, ) -from pandas.compat import IS64 from pandas.tseries.offsets import ( FY5253, @@ -112,7 +110,7 @@ def d(_offset): return Timestamp(2008, 1, 2) -def test_apply_out_of_range(request, tz_naive_fixture, _offset): +def test_apply_out_of_range(tz_naive_fixture, _offset): tz = tz_naive_fixture # try to create an out-of-bounds result timestamp; if we can't create @@ -133,13 +131,6 @@ def test_apply_out_of_range(request, tz_naive_fixture, _offset): t = Timestamp("20080101", tz=tz) result = t + offset assert isinstance(result, datetime) - - if isinstance(tz, tzlocal) and not IS64: - # If we hit OutOfBoundsDatetime on non-64 bit machines - # we'll drop out of the try clause before the next test - request.node.add_marker( - pytest.mark.xfail(reason="OverflowError inside tzlocal past 2038") - ) assert str(t.tzinfo) == str(result.tzinfo) except OutOfBoundsDatetime: From ab3ce276a891728be2f61e7b9c1264bb7e1168bd Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 10 Oct 2022 17:44:41 -0700 Subject: [PATCH 3/4] Revert "test passing now" This reverts commit 67dd5d3fd6b6c18df77a4169108c31d771bdb7a1. --- pandas/tests/tseries/offsets/test_common.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py index aa900f54bc8e7..f886034fe07fa 100644 --- a/pandas/tests/tseries/offsets/test_common.py +++ b/pandas/tests/tseries/offsets/test_common.py @@ -1,11 +1,13 @@ from datetime import datetime +from dateutil.tz.tz import tzlocal import pytest from pandas._libs.tslibs import ( OutOfBoundsDatetime, Timestamp, ) +from pandas.compat import IS64 from pandas.tseries.offsets import ( FY5253, @@ -110,7 +112,7 @@ def d(_offset): return Timestamp(2008, 1, 2) -def test_apply_out_of_range(tz_naive_fixture, _offset): +def test_apply_out_of_range(request, tz_naive_fixture, _offset): tz = tz_naive_fixture # try to create an out-of-bounds result timestamp; if we can't create @@ -131,6 +133,13 @@ def test_apply_out_of_range(tz_naive_fixture, _offset): t = Timestamp("20080101", tz=tz) result = t + offset assert isinstance(result, datetime) + + if isinstance(tz, tzlocal) and not IS64: + # If we hit OutOfBoundsDatetime on non-64 bit machines + # we'll drop out of the try clause before the next test + request.node.add_marker( + pytest.mark.xfail(reason="OverflowError inside tzlocal past 2038") + ) assert str(t.tzinfo) == str(result.tzinfo) except OutOfBoundsDatetime: From dc1e6b1edf24bbc4dd00d0b3f6d703b061dd9926 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 10 Oct 2022 17:46:12 -0700 Subject: [PATCH 4/4] Refine xfail comment --- pandas/tests/tseries/offsets/test_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_common.py b/pandas/tests/tseries/offsets/test_common.py index f886034fe07fa..1c140d7dda330 100644 --- a/pandas/tests/tseries/offsets/test_common.py +++ b/pandas/tests/tseries/offsets/test_common.py @@ -134,7 +134,7 @@ def test_apply_out_of_range(request, tz_naive_fixture, _offset): result = t + offset assert isinstance(result, datetime) - if isinstance(tz, tzlocal) and not IS64: + if isinstance(tz, tzlocal) and not IS64 and _offset is not DateOffset: # If we hit OutOfBoundsDatetime on non-64 bit machines # we'll drop out of the try clause before the next test request.node.add_marker(