From 7341a63937f7d12bd99aa23f8411140e9383f8fc Mon Sep 17 00:00:00 2001 From: Oli Russell Date: Wed, 9 Jul 2025 12:07:37 +0100 Subject: [PATCH 1/2] Add option for deterministic tick I was somewhat surprised to find that `tick` wasn't deterministic and actually increments by time passed. At $WORK, this was causing some tests to become flakey and not reproducible. This commit adds an optional `tick_delta` argument such that it's possible to make the tick deterministic. --- README.rst | 12 +++++++++++- src/time_machine/__init__.py | 11 ++++++++++- tests/test_time_machine.py | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 093f0eb5..e45064d8 100644 --- a/README.rst +++ b/README.rst @@ -63,7 +63,7 @@ Usage If you’re coming from freezegun or libfaketime, see also the below section on migrating. -``travel(destination, *, tick=True)`` +``travel(destination, *, tick=True, tick_delta=None)`` ------------------------------------- ``travel()`` is a class that allows time travel, to the datetime specified by ``destination``. @@ -98,6 +98,16 @@ If ``True``, the default, successive calls to mocked functions return values inc So after starting travel to ``0.0`` (the UNIX epoch), the first call to any datetime function will return its representation of ``1970-01-01 00:00:00.000000`` exactly. The following calls "tick," so if a call was made exactly half a second later, it would return ``1970-01-01 00:00:00.500000``. +If ``tick`` is ``True``, setting ``tick_delta`` makes the tick deterministic, for example: + +.. code-block:: python + + with time_machine.travel(dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1)): + assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=0) + assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=1) + assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=2) + ... + Mocked Functions ^^^^^^^^^^^^^^^^ diff --git a/src/time_machine/__init__.py b/src/time_machine/__init__.py index 849624d6..968d2f31 100644 --- a/src/time_machine/__init__.py +++ b/src/time_machine/__init__.py @@ -125,12 +125,14 @@ def __init__( destination_timestamp: float, destination_tzname: str | None, tick: bool, + tick_delta: dt.timedelta | None = None, ) -> None: self._destination_timestamp_ns = int( destination_timestamp * NANOSECONDS_PER_SECOND ) self._destination_tzname = destination_tzname self._tick = tick + self._tick_delta = tick_delta self._requested = False def time(self) -> float: @@ -140,6 +142,11 @@ def time_ns(self) -> int: if not self._tick: return self._destination_timestamp_ns + if self._tick_delta is not None: + destination_timestamp_ns_before = self._destination_timestamp_ns + self._destination_timestamp_ns += int(self._tick_delta.total_seconds() * NANOSECONDS_PER_SECOND) + return destination_timestamp_ns_before + base = SYSTEM_EPOCH_TIMESTAMP_NS + self._destination_timestamp_ns now_ns: int = _time_machine.original_time_ns() @@ -200,11 +207,12 @@ def _stop(self) -> None: class travel: - def __init__(self, destination: DestinationType, *, tick: bool = True) -> None: + def __init__(self, destination: DestinationType, *, tick: bool = True, tick_delta: dt.timedelta | None = None) -> None: self.destination_timestamp, self.destination_tzname = extract_timestamp_tzname( destination ) self.tick = tick + self.tick_delta = tick_delta def start(self) -> Coordinates: _time_machine.patch_if_needed() @@ -217,6 +225,7 @@ def start(self) -> Coordinates: destination_timestamp=self.destination_timestamp, destination_tzname=self.destination_tzname, tick=self.tick, + tick_delta=self.tick_delta, ) coordinates_stack.append(coordinates) coordinates._start() diff --git a/tests/test_time_machine.py b/tests/test_time_machine.py index f4b86ce5..33a5a43e 100644 --- a/tests/test_time_machine.py +++ b/tests/test_time_machine.py @@ -337,6 +337,20 @@ def test_time_time_ns_no_tick(): assert time.time_ns() == int(EPOCH * NANOSECONDS_PER_SECOND) +def test_tick_delta() -> None: + ts = list[dt.datetime]() + with time_machine.travel(dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1)): + for _ in range(5): + ts.append(dt.datetime.now()) + assert ts == [ + dt.datetime(2023, 1, 1, 0, 0, microsecond=0), + dt.datetime(2023, 1, 1, 0, 0, microsecond=1), + dt.datetime(2023, 1, 1, 0, 0, microsecond=2), + dt.datetime(2023, 1, 1, 0, 0, microsecond=3), + dt.datetime(2023, 1, 1, 0, 0, microsecond=4), + ] + + # all supported forms From 553a459b2b6be31959f8bff6aa95ce94836b5f01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:12:03 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.rst | 4 +++- src/time_machine/__init__.py | 12 ++++++++++-- tests/test_time_machine.py | 4 +++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index e45064d8..c5f7beee 100644 --- a/README.rst +++ b/README.rst @@ -102,7 +102,9 @@ If ``tick`` is ``True``, setting ``tick_delta`` makes the tick deterministic, fo .. code-block:: python - with time_machine.travel(dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1)): + with time_machine.travel( + dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1) + ): assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=0) assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=1) assert dt.datetime.now() == dt.datetime(2023, 1, 1, 0, 0, microsecond=2) diff --git a/src/time_machine/__init__.py b/src/time_machine/__init__.py index 968d2f31..97d1de76 100644 --- a/src/time_machine/__init__.py +++ b/src/time_machine/__init__.py @@ -144,7 +144,9 @@ def time_ns(self) -> int: if self._tick_delta is not None: destination_timestamp_ns_before = self._destination_timestamp_ns - self._destination_timestamp_ns += int(self._tick_delta.total_seconds() * NANOSECONDS_PER_SECOND) + self._destination_timestamp_ns += int( + self._tick_delta.total_seconds() * NANOSECONDS_PER_SECOND + ) return destination_timestamp_ns_before base = SYSTEM_EPOCH_TIMESTAMP_NS + self._destination_timestamp_ns @@ -207,7 +209,13 @@ def _stop(self) -> None: class travel: - def __init__(self, destination: DestinationType, *, tick: bool = True, tick_delta: dt.timedelta | None = None) -> None: + def __init__( + self, + destination: DestinationType, + *, + tick: bool = True, + tick_delta: dt.timedelta | None = None, + ) -> None: self.destination_timestamp, self.destination_tzname = extract_timestamp_tzname( destination ) diff --git a/tests/test_time_machine.py b/tests/test_time_machine.py index 33a5a43e..e19a7bc5 100644 --- a/tests/test_time_machine.py +++ b/tests/test_time_machine.py @@ -339,7 +339,9 @@ def test_time_time_ns_no_tick(): def test_tick_delta() -> None: ts = list[dt.datetime]() - with time_machine.travel(dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1)): + with time_machine.travel( + dt.datetime(2023, 1, 1), tick_delta=dt.timedelta(microseconds=1) + ): for _ in range(5): ts.append(dt.datetime.now()) assert ts == [