From 2d6e6aa1c4355c46b66c75d0832f4d768f634d29 Mon Sep 17 00:00:00 2001 From: Uttkarsh Joshi Date: Wed, 15 Jul 2026 13:20:38 +0530 Subject: [PATCH 1/5] Fix DateTime ordering across DST fold transitions: normalize comparisons to UTC via astimezone and add regression test for DST fold --- src/pendulum/datetime.py | 42 +++++++++++++++++++++++++++++++ tests/datetime/test_comparison.py | 25 ++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/pendulum/datetime.py b/src/pendulum/datetime.py index da89b13d..3a003b99 100644 --- a/src/pendulum/datetime.py +++ b/src/pendulum/datetime.py @@ -554,6 +554,48 @@ def is_anniversary( # type: ignore[override] return (self.month, self.day) == (instance.month, instance.day) + # Rich ordering operations + def __lt__(self, other: datetime.datetime) -> bool: # type: ignore[override] + if not isinstance(other, datetime.datetime): + return NotImplemented + + # Normalize both operands to UTC stdlib datetimes using base implementation + self_utc = datetime.datetime.astimezone(self, UTC) + other_dt = other if isinstance(other, DateTime) else DateTime.instance(other) + other_utc = datetime.datetime.astimezone(other_dt, UTC) + + return datetime.datetime.__lt__(self_utc, other_utc) + + def __le__(self, other: datetime.datetime) -> bool: # type: ignore[override] + if not isinstance(other, datetime.datetime): + return NotImplemented + + self_utc = datetime.datetime.astimezone(self, UTC) + other_dt = other if isinstance(other, DateTime) else DateTime.instance(other) + other_utc = datetime.datetime.astimezone(other_dt, UTC) + + return datetime.datetime.__le__(self_utc, other_utc) + + def __gt__(self, other: datetime.datetime) -> bool: # type: ignore[override] + if not isinstance(other, datetime.datetime): + return NotImplemented + + self_utc = datetime.datetime.astimezone(self, UTC) + other_dt = other if isinstance(other, DateTime) else DateTime.instance(other) + other_utc = datetime.datetime.astimezone(other_dt, UTC) + + return datetime.datetime.__gt__(self_utc, other_utc) + + def __ge__(self, other: datetime.datetime) -> bool: # type: ignore[override] + if not isinstance(other, datetime.datetime): + return NotImplemented + + self_utc = datetime.datetime.astimezone(self, UTC) + other_dt = other if isinstance(other, DateTime) else DateTime.instance(other) + other_utc = datetime.datetime.astimezone(other_dt, UTC) + + return datetime.datetime.__ge__(self_utc, other_utc) + # ADDITIONS AND SUBSTRACTIONS def add( diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 4e25f0d6..9401e93d 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -392,3 +392,28 @@ def test_comparison_to_unsupported(): assert dt1 != "test" assert dt1 not in ["test"] + + +def test_less_than_with_fold(): + d1 = pendulum.datetime( + 2023, + 11, + 5, + 1, + 15, + tz="America/Los_Angeles", + fold=1, + ) + + d2 = pendulum.datetime( + 2023, + 11, + 5, + 1, + 25, + tz="America/Los_Angeles", + fold=0, + ) + + assert d1.timestamp() > d2.timestamp() + assert not (d1 < d2) From eb1c3d51dc94a54ce2f888c5f046756bd2bc2ebc Mon Sep 17 00:00:00 2001 From: Uttkarsh Joshi Date: Wed, 15 Jul 2026 13:59:32 +0530 Subject: [PATCH 2/5] Add test.sh for Olympus and Dockerfile using base image Poetry --- Dockerfile | 21 +++++++++++++++++++++ test.sh | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 Dockerfile create mode 100755 test.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d68ac341 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +ARG BASE_IMAGE +FROM ${BASE_IMAGE} + +WORKDIR /app + +# Copy poetry metadata first for potential layer caching +COPY pyproject.toml poetry.lock ./ + +# Install project dependencies using Poetry (assumes Poetry exists in base image). +# Do NOT install Poetry manually. +RUN poetry config virtualenvs.in-project false \ + && poetry install --only main --only test --only build --no-root -v + +# Copy the full repository into the image +COPY . . + +# Install the local package in editable mode so tests can import it +RUN poetry run pip install -e . + +# Do not run tests in the image build; open a shell by default +CMD ["/bin/bash"] diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..6080195c --- /dev/null +++ b/test.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# POSIX shell script for Olympus test runner. +# Usage: +# ./test.sh base +# ./test.sh new +# This script runs only the regression test and does not install packages. +set -eu + +if [ "$#" -ne 1 ]; then + printf 'Usage: %s [base|new]\n' "$0" >&2 + exit 2 +fi + +case "$1" in + base|new) + ;; + *) + printf 'Invalid argument: %s\n' "$1" >&2 + printf 'Usage: %s [base|new]\n' "$0" >&2 + exit 2 + ;; +esac + +poetry run pytest -q tests/datetime/test_comparison.py::test_less_than_with_fold +exit $? From f7ee5498afb0d9fd58f90f4443a2263787d731f1 Mon Sep 17 00:00:00 2001 From: Uttkarsh Joshi Date: Wed, 15 Jul 2026 14:03:40 +0530 Subject: [PATCH 3/5] Use Olympus base image and simplify Poetry install --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d68ac341..8b752d29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ -ARG BASE_IMAGE -FROM ${BASE_IMAGE} +FROM public.ecr.aws/d3j8x8q7/olympus-base-python:latest WORKDIR /app @@ -9,7 +8,7 @@ COPY pyproject.toml poetry.lock ./ # Install project dependencies using Poetry (assumes Poetry exists in base image). # Do NOT install Poetry manually. RUN poetry config virtualenvs.in-project false \ - && poetry install --only main --only test --only build --no-root -v + && poetry install --no-root -v # Copy the full repository into the image COPY . . From a2e48cb9798d6c3c7992d690e6b60668bb02046f Mon Sep 17 00:00:00 2001 From: Uttkarsh Joshi Date: Wed, 15 Jul 2026 14:08:58 +0530 Subject: [PATCH 4/5] Strengthen DST fold comparison test --- tests/datetime/test_comparison.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 9401e93d..05556ee6 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -416,4 +416,11 @@ def test_less_than_with_fold(): ) assert d1.timestamp() > d2.timestamp() + + # Verify rich ordering operators behave consistently when fold differs but + # the absolute instant (timestamp) shows d1 is later than d2. assert not (d1 < d2) + assert d1 > d2 + assert d2 < d1 + assert d1 >= d2 + assert not (d1 <= d2) From 6f3a680280a950d5f0ac2fde24c0c0e185f02d6e Mon Sep 17 00:00:00 2001 From: Uttkarsh Joshi Date: Wed, 15 Jul 2026 14:18:43 +0530 Subject: [PATCH 5/5] Make test.sh run full comparison test file for base/new modes --- Dockerfile | 16 ++++------------ test.sh | 27 +++++++++++++-------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8b752d29..e383edb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,19 +2,11 @@ FROM public.ecr.aws/d3j8x8q7/olympus-base-python:latest WORKDIR /app -# Copy poetry metadata first for potential layer caching -COPY pyproject.toml poetry.lock ./ - -# Install project dependencies using Poetry (assumes Poetry exists in base image). -# Do NOT install Poetry manually. -RUN poetry config virtualenvs.in-project false \ - && poetry install --no-root -v - -# Copy the full repository into the image +# Copy repository into the image before installing COPY . . -# Install the local package in editable mode so tests can import it -RUN poetry run pip install -e . +# Use pip to install in editable mode with test extras to avoid Poetry issues +RUN python -m pip install --upgrade pip +RUN python -m pip install -e ".[test]" -# Do not run tests in the image build; open a shell by default CMD ["/bin/bash"] diff --git a/test.sh b/test.sh index 6080195c..f2fdead4 100755 --- a/test.sh +++ b/test.sh @@ -1,25 +1,24 @@ #!/bin/sh -# POSIX shell script for Olympus test runner. -# Usage: -# ./test.sh base -# ./test.sh new -# This script runs only the regression test and does not install packages. set -eu if [ "$#" -ne 1 ]; then - printf 'Usage: %s [base|new]\n' "$0" >&2 - exit 2 + echo "Usage: $0 {base|new}" + exit 1 fi case "$1" in - base|new) +base) + # Run the comparison file but exclude the new regression test that + # doesn't exist in the base commit. + poetry run pytest -q tests/datetime/test_comparison.py -k "not less_than_with_fold" ;; - *) - printf 'Invalid argument: %s\n' "$1" >&2 - printf 'Usage: %s [base|new]\n' "$0" >&2 - exit 2 +new) + # Run the full file including the new regression test. + poetry run pytest -q tests/datetime/test_comparison.py + ;; +*) + echo "Usage: $0 {base|new}" + exit 1 ;; esac -poetry run pytest -q tests/datetime/test_comparison.py::test_less_than_with_fold -exit $?