From 0387f04c0f8a397d3db840754d863f2998c5719a Mon Sep 17 00:00:00 2001 From: Eric Sauser Date: Mon, 3 Jan 2022 11:23:33 -0600 Subject: [PATCH 1/9] Fix comparison acorss Daylight Savings Time boundary --- pendulum/datetime.py | 36 ++++++++++++++++--------------- tests/datetime/test_comparison.py | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index feb140fe..dd199451 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1536,26 +1536,28 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return self.__class__, self._getstate(protocol) - def _cmp(self, other, **kwargs): - # Fix for pypy which compares using this method - # which would lead to infinite recursion if we didn't override - kwargs = {"tzinfo": self.tz} + def __le__(self, other): + if isinstance(other, DateTime): + return self._cmp(other) <= 0 + return super().__le__(other) - if _HAS_FOLD: - kwargs["fold"] = self.fold + def __lt__(self, other): + if isinstance(other, DateTime): + return self._cmp(other) < 0 + return super().__lt__(other) - dt = datetime.datetime( - self.year, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.microsecond, - **kwargs - ) + def __ge__(self, other): + # Will default to the negative of its reflection + return NotImplemented + + def __gt__(self, other): + # Will default to the negative of its reflection + return NotImplemented - return 0 if dt == other else 1 if dt > other else -1 + def _cmp(self, other, **kwargs): + sts = self.timestamp() + ots = other.timestamp() + return 0 if sts == ots else 1 if sts > ots else -1 DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 0819f801..4411385b 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -2,6 +2,7 @@ import pendulum import pytz +import pytest from ..conftest import assert_datetime @@ -192,6 +193,27 @@ def test_less_than_with_timezone_false(): assert not d1 < d3 +@pytest.mark.parametrize( + 'truth_fun', + ( + lambda earlier, later: earlier < later, + lambda earlier, later: earlier <= later, + lambda earlier, later: later > earlier, + lambda earlier, later: later >= earlier, + ) +) +def test_comparison_crossing_dst_transitioning_off(truth_fun): + # We only need to test turning off DST, since that's when the time + # component goes backwards. + # We start with 2019-11-03T01:30:00-0700 + earlier = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific") + # Adding 55 minutes to it, we turn off DST, but the time component is + # slightly less than before, i.e. we get 2019-11-03T01:25:00-0800 + later = earlier.add(minutes=55) + # Run through all inequality-comparison functions + assert truth_fun(earlier, later) + + def test_less_than_or_equal_true(): d1 = pendulum.datetime(2000, 1, 1) d2 = pendulum.datetime(2000, 1, 2) From ac0f4cc239347fc34489c66f057018c2d966b1b4 Mon Sep 17 00:00:00 2001 From: Eric Sauser Date: Tue, 4 Jan 2022 09:39:27 -0600 Subject: [PATCH 2/9] Added datetime.datetime tests --- tests/datetime/test_comparison.py | 75 ++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 4411385b..944aa697 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -1,9 +1,12 @@ -from datetime import datetime +from datetime import datetime, timedelta, tzinfo +from dateutil import tz +from pytz import timezone import pendulum import pytz import pytest + from ..conftest import assert_datetime @@ -202,7 +205,7 @@ def test_less_than_with_timezone_false(): lambda earlier, later: later >= earlier, ) ) -def test_comparison_crossing_dst_transitioning_off(truth_fun): +def test_comparison_crossing_dst_transitioning_off_pendulum_pendulum(truth_fun): # We only need to test turning off DST, since that's when the time # component goes backwards. # We start with 2019-11-03T01:30:00-0700 @@ -214,6 +217,74 @@ def test_comparison_crossing_dst_transitioning_off(truth_fun): assert truth_fun(earlier, later) +@pytest.mark.parametrize( + 'truth_fun', + ( + lambda earlier, later: earlier < later, + lambda earlier, later: earlier <= later, + lambda earlier, later: later > earlier, + lambda earlier, later: later >= earlier, + ) +) +def test_comparison_crossing_dst_transitioning_off_pendulum_datetime(truth_fun): + # We only need to test turning off DST, since that's when the time + # component goes backwards. + # We start with 2019-11-03T01:30:00-0700 + earlier_pendulum = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific") + us_pacific = timezone("US/Pacific") + earlier_datetime = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific) + # Adding 55 minutes to it, we turn off DST, but the time component is + # slightly less than before, i.e. we get 2019-11-03T01:25:00-0800 + later_datetime = earlier_datetime + timedelta(minutes=55) + # Run through all inequality-comparison functions + assert truth_fun(earlier_pendulum, later_datetime) + + +@pytest.mark.parametrize( + 'truth_fun', + ( + lambda earlier, later: earlier < later, + lambda earlier, later: earlier <= later, + lambda earlier, later: later > earlier, + lambda earlier, later: later >= earlier, + ) +) +def test_comparison_crossing_dst_transitioning_off_datetime_pendulum(truth_fun): + # We only need to test turning off DST, since that's when the time + # component goes backwards. + # We start with 2019-11-03T01:30:00-0700 + earlier_pendulum = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific") + us_pacific = timezone("US/Pacific") + earlier_datetime = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific) + # Adding 55 minutes to it, we turn off DST, but the time component is + # slightly less than before, i.e. we get 2019-11-03T01:25:00-0800 + later_pendulum = earlier_pendulum.add(minutes=55) + # Run through all inequality-comparison functions + assert truth_fun(earlier_datetime, later_pendulum) + + +@pytest.mark.parametrize( + 'truth_fun', + ( + lambda earlier, later: earlier < later, + lambda earlier, later: earlier <= later, + lambda earlier, later: later > earlier, + lambda earlier, later: later >= earlier, + ) +) +def test_comparison_crossing_dst_transitioning_off_datetime_datetime(truth_fun): + # We only need to test turning off DST, since that's when the time + # component goes backwards. + # We start with 2019-11-03T01:30:00-0700 + us_pacific = timezone("US/Pacific") + earlier = datetime(2019, 11, 3, 1, 30, tzinfo=us_pacific) + # Adding 55 minutes to it, we turn off DST, but the time component is + # slightly less than before, i.e. we get 2019-11-03T01:25:00-0800 + later = earlier + timedelta(minutes=55) + # Run through all inequality-comparison functions + assert truth_fun(earlier, later) + + def test_less_than_or_equal_true(): d1 = pendulum.datetime(2000, 1, 1) d2 = pendulum.datetime(2000, 1, 2) From 220b4bcba446e748101996e44fc33cf89558341e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 03:41:58 +0000 Subject: [PATCH 3/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pendulum/datetime.py | 1 - tests/datetime/test_comparison.py | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 38bfe2b4..74516aec 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -4,7 +4,6 @@ import datetime from typing import TYPE_CHECKING -from typing import Any from typing import Callable from typing import Optional from typing import cast diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 2cdc35db..7050363d 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -1,12 +1,12 @@ from __future__ import annotations -from datetime import datetime, timedelta, tzinfo -from dateutil import tz -from pytz import timezone +from datetime import datetime +from datetime import timedelta -import pytz import pytest +import pytz +from pytz import timezone import pendulum @@ -200,13 +200,13 @@ def test_less_than_with_timezone_false(): @pytest.mark.parametrize( - 'truth_fun', + "truth_fun", ( lambda earlier, later: earlier < later, lambda earlier, later: earlier <= later, lambda earlier, later: later > earlier, lambda earlier, later: later >= earlier, - ) + ), ) def test_comparison_crossing_dst_transitioning_off_pendulum_pendulum(truth_fun): # We only need to test turning off DST, since that's when the time @@ -221,13 +221,13 @@ def test_comparison_crossing_dst_transitioning_off_pendulum_pendulum(truth_fun): @pytest.mark.parametrize( - 'truth_fun', + "truth_fun", ( lambda earlier, later: earlier < later, lambda earlier, later: earlier <= later, lambda earlier, later: later > earlier, lambda earlier, later: later >= earlier, - ) + ), ) def test_comparison_crossing_dst_transitioning_off_pendulum_datetime(truth_fun): # We only need to test turning off DST, since that's when the time @@ -244,13 +244,13 @@ def test_comparison_crossing_dst_transitioning_off_pendulum_datetime(truth_fun): @pytest.mark.parametrize( - 'truth_fun', + "truth_fun", ( lambda earlier, later: earlier < later, lambda earlier, later: earlier <= later, lambda earlier, later: later > earlier, lambda earlier, later: later >= earlier, - ) + ), ) def test_comparison_crossing_dst_transitioning_off_datetime_pendulum(truth_fun): # We only need to test turning off DST, since that's when the time @@ -267,13 +267,13 @@ def test_comparison_crossing_dst_transitioning_off_datetime_pendulum(truth_fun): @pytest.mark.parametrize( - 'truth_fun', + "truth_fun", ( lambda earlier, later: earlier < later, lambda earlier, later: earlier <= later, lambda earlier, later: later > earlier, lambda earlier, later: later >= earlier, - ) + ), ) def test_comparison_crossing_dst_transitioning_off_datetime_datetime(truth_fun): # We only need to test turning off DST, since that's when the time From 9324328196bfcfea12819ca048848c74828ec282 Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 21:46:11 -0600 Subject: [PATCH 4/9] Update datetime.py --- pendulum/datetime.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 74516aec..0d1ebdb0 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1373,10 +1373,22 @@ def __gt__(self, other): # Will default to the negative of its reflection return NotImplemented - def _cmp(self, other, **kwargs): - sts = self.timestamp() - ots = other.timestamp() - return 0 if sts == ots else 1 if sts > ots else -1 + def _cmp(self, other: datetime.datetime, **kwargs: Any) -> int: + # Fix for pypy which compares using this method + # which would lead to infinite recursion if we didn't override + dt = datetime.datetime( + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + tzinfo=self.tz, + fold=self.fold, + ) + + return 0 if dt == other else 1 if dt > other else -1 DateTime.min: DateTime = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) # type: ignore[misc] From a85d139015c526d63f99db1ea4e2a3dbd2b8b1f3 Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 21:55:23 -0600 Subject: [PATCH 5/9] Update datetime.py --- pendulum/datetime.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 0d1ebdb0..489b5016 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -4,6 +4,7 @@ import datetime from typing import TYPE_CHECKING +from typing import Any from typing import Callable from typing import Optional from typing import cast @@ -1355,21 +1356,21 @@ def __reduce_ex__( # type: ignore[override] ]: return self.__class__, self._getstate(protocol) - def __le__(self, other): + def __le__(self, other: DateTime) -> Bool: if isinstance(other, DateTime): return self._cmp(other) <= 0 return super().__le__(other) - def __lt__(self, other): + def __lt__(self, other: DateTime) -> Bool: if isinstance(other, DateTime): return self._cmp(other) < 0 return super().__lt__(other) - def __ge__(self, other): + def __ge__(self, other: DateTime): # Will default to the negative of its reflection return NotImplemented - def __gt__(self, other): + def __gt__(self, other: DateTime): # Will default to the negative of its reflection return NotImplemented From 004644e017c4d02b2f718a93230e468277b14425 Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 21:58:29 -0600 Subject: [PATCH 6/9] Update datetime.py --- pendulum/datetime.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 489b5016..a85f2f93 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1356,21 +1356,21 @@ def __reduce_ex__( # type: ignore[override] ]: return self.__class__, self._getstate(protocol) - def __le__(self, other: DateTime) -> Bool: + def __le__(self, other: datetime.datetime) -> bool: if isinstance(other, DateTime): return self._cmp(other) <= 0 return super().__le__(other) - def __lt__(self, other: DateTime) -> Bool: + def __lt__(self, other: datetime.datetime) -> bool: if isinstance(other, DateTime): return self._cmp(other) < 0 return super().__lt__(other) - def __ge__(self, other: DateTime): + def __ge__(self, other: datetime.datetime) -> bool: # Will default to the negative of its reflection return NotImplemented - def __gt__(self, other: DateTime): + def __gt__(self, other: datetime.datetime) -> bool: # Will default to the negative of its reflection return NotImplemented From e7f34bc2eb984284e5d73426338afdd3bb03af6f Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 22:01:31 -0600 Subject: [PATCH 7/9] Update datetime.py --- pendulum/datetime.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index a85f2f93..313db485 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1356,21 +1356,21 @@ def __reduce_ex__( # type: ignore[override] ]: return self.__class__, self._getstate(protocol) - def __le__(self, other: datetime.datetime) -> bool: + def __le__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) <= 0 return super().__le__(other) - def __lt__(self, other: datetime.datetime) -> bool: + def __lt__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) < 0 return super().__lt__(other) - def __ge__(self, other: datetime.datetime) -> bool: + def __ge__(self, other: datetime.date) -> bool: # Will default to the negative of its reflection return NotImplemented - def __gt__(self, other: datetime.datetime) -> bool: + def __gt__(self, other: datetime.date) -> bool: # Will default to the negative of its reflection return NotImplemented From 8a3679c5cd0c6d45eefcacf2620614dbce6370cd Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 22:03:45 -0600 Subject: [PATCH 8/9] Update datetime.py --- pendulum/datetime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 313db485..1d5fc2e2 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1359,12 +1359,12 @@ def __reduce_ex__( # type: ignore[override] def __le__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) <= 0 - return super().__le__(other) + return super().__le__(other.date) def __lt__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) < 0 - return super().__lt__(other) + return super().__lt__(other.date) def __ge__(self, other: datetime.date) -> bool: # Will default to the negative of its reflection From c74571935bf97dc301eb2aad9a42e36632ddbf98 Mon Sep 17 00:00:00 2001 From: esauser Date: Thu, 1 Dec 2022 22:05:00 -0600 Subject: [PATCH 9/9] Update datetime.py --- pendulum/datetime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index 1d5fc2e2..2bd88bde 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1359,12 +1359,12 @@ def __reduce_ex__( # type: ignore[override] def __le__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) <= 0 - return super().__le__(other.date) + return super().__le__(other.date()) def __lt__(self, other: datetime.date) -> bool: if isinstance(other, DateTime): return self._cmp(other) < 0 - return super().__lt__(other.date) + return super().__lt__(other.date()) def __ge__(self, other: datetime.date) -> bool: # Will default to the negative of its reflection