From 9d6a0e0a73f33d83b0e838e5603067ea3972a174 Mon Sep 17 00:00:00 2001 From: fan wu <336798967+FanWu-ai@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:59:29 +0900 Subject: [PATCH] Honor freeze requests during active time travel --- docs/docs/testing.md | 4 ++ src/pendulum/testing/traveller.py | 7 ++- tests/testing/test_time_travel.py | 82 +++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/docs/docs/testing.md b/docs/docs/testing.md index 25aad8d69..392aef701 100644 --- a/docs/docs/testing.md +++ b/docs/docs/testing.md @@ -55,6 +55,10 @@ stopping the time, use the `freeze` parameter: >>> pendulum.travel_to(pendulum.yesterday(), freeze=True) ``` +Passing `freeze=True` also stops a clock that is already travelling. Once frozen, +subsequent calls to `travel()` or `travel_to()` keep the clock frozen, even when +`freeze` is omitted. Use `travel_back()` before starting a new ticking clock. + ## Travelling back to the present Using any of the travel helpers will keep you in the past, or future, until you decide diff --git a/src/pendulum/testing/traveller.py b/src/pendulum/testing/traveller.py index 6d48ea3da..b2de9278e 100644 --- a/src/pendulum/testing/traveller.py +++ b/src/pendulum/testing/traveller.py @@ -115,7 +115,8 @@ def travel( minutes=minutes, seconds=seconds, microseconds=microseconds, - ) + ), + tick=False if freeze else None, ) return self @@ -123,7 +124,9 @@ def travel( def travel_to(self, dt: DateTime, *, freeze: bool = False) -> Self: self._start(freeze=freeze) - cast("time_machine.Traveller", self._traveller).move_to(dt) + cast("time_machine.Traveller", self._traveller).move_to( + dt, tick=False if freeze else None + ) return self diff --git a/tests/testing/test_time_travel.py b/tests/testing/test_time_travel.py index 2fe7b8cc6..41579269d 100644 --- a/tests/testing/test_time_travel.py +++ b/tests/testing/test_time_travel.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import count from time import sleep from typing import TYPE_CHECKING @@ -7,6 +8,7 @@ import pendulum +from pendulum.testing.traveller import Traveller from pendulum.utils._compat import PYPY @@ -84,3 +86,83 @@ def test_freeze() -> None: sleep(0.01) assert now != pendulum.now() + + +@pytest.fixture +def ticking_clock(monkeypatch: pytest.MonkeyPatch) -> None: + # Advance time-machine's real clock deterministically, without sleeping. + ticks = count(1_700_000_000_000_000_000, 1_000_000_000) + monkeypatch.setattr( + "time_machine._time_machine.original_time_ns", lambda: next(ticks) + ) + + +@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy") +@pytest.mark.parametrize("method", ["travel", "travel_to"]) +@pytest.mark.parametrize("initial_freeze", [None, False, True]) +@pytest.mark.parametrize("freeze", [False, True]) +def test_travel_freeze_state( + ticking_clock: None, method: str, initial_freeze: bool | None, freeze: bool +) -> None: + if initial_freeze is not None: + pendulum.travel_to(pendulum.datetime(2024, 1, 1, 12), freeze=initial_freeze) + + if method == "travel": + pendulum.travel(days=2, freeze=freeze) + else: + pendulum.travel_to(pendulum.datetime(2024, 2, 1, 12), freeze=freeze) + + first = pendulum.now("UTC") + second = pendulum.now("UTC") + if freeze or initial_freeze: + assert second == first + else: + assert second == first.add(seconds=1) + + if method == "travel_to": + assert first == pendulum.datetime(2024, 2, 1, 12) + + +@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy") +@pytest.mark.parametrize("method", ["travel", "travel_to"]) +def test_freeze_after_repeated_travel(ticking_clock: None, method: str) -> None: + target = pendulum.datetime(2024, 1, 1, 12) + pendulum.travel_to(target) + pendulum.travel(days=1) + if method == "travel": + pendulum.travel(days=1, freeze=True) + else: + pendulum.travel_to(target, freeze=True) + + frozen = pendulum.now("UTC") + pendulum.travel(hours=2) + assert pendulum.now("UTC") == frozen.add(hours=2) + assert pendulum.now("UTC") == frozen.add(hours=2) + pendulum.travel_to(target) + assert pendulum.now("UTC") == target + assert pendulum.now("UTC") == target + + pendulum.travel_back() + pendulum.travel_to(target) + assert pendulum.now("UTC") == target + assert pendulum.now("UTC") == target.add(seconds=1) + + +@pytest.mark.skipif(PYPY, reason="Time travelling not available on PyPy") +@pytest.mark.parametrize("method", ["travel", "travel_to"]) +def test_freeze_active_traveller_in_nested_context( + ticking_clock: None, method: str +) -> None: + outer_target = pendulum.datetime(2024, 1, 1, 12) + inner_target = pendulum.datetime(2024, 2, 1, 12) + with Traveller().travel_to(outer_target): + with Traveller().travel_to(inner_target) as inner: + if method == "travel": + assert inner.travel(hours=2, freeze=True) is inner + else: + assert inner.travel_to(inner_target, freeze=True) is inner + assert pendulum.now("UTC") == pendulum.now("UTC") + + first = pendulum.now("UTC") + assert first.date() == outer_target.date() + assert pendulum.now("UTC") == first.add(seconds=1)