From c9636086130edad9d4fca44368995b612ebc484b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 25 Apr 2024 13:52:20 +0200 Subject: [PATCH 1/6] Fix `tick` delta type handling ## `FrozenDateTimeFactory.tick` The `delta` argument is capable of handling `float`s. In previous versions of freezgun, the `.pyi` type annotations were correctly reflecting that. For some reason, when moving the type annotations into the `.py` file, this information got lost. Further, checking for `isinstance(delta, numbers.Real)` is probably not what was intended as `fraction.Fraction` is a subclass of `Real`, but will cause an error when passed into `datetime.timedelta(seconds=delta)`. ## `StepTickTimeFactory.tick` The same issue with the type hint applies here. Fruther, passing an integer/float `delta` would lead to that number being added to the frozen `datetime.datetime`, which is not a valid operation (`TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'int'`). --- freezegun/api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freezegun/api.py b/freezegun/api.py index 0f11e0c..e48a871 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -531,8 +531,8 @@ def __init__(self, time_to_freeze: datetime.datetime): def __call__(self) -> datetime.datetime: return self.time_to_freeze - def tick(self, delta: Union[datetime.timedelta, int]=datetime.timedelta(seconds=1)) -> datetime.datetime: - if isinstance(delta, numbers.Real): + def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: + if isinstance(delta, float): # noinspection PyTypeChecker self.time_to_freeze += datetime.timedelta(seconds=delta) else: @@ -557,9 +557,11 @@ def __call__(self) -> datetime.datetime: self.tick() return return_time - def tick(self, delta: Union[datetime.timedelta, int, None]=None) -> datetime.datetime: + def tick(self, delta: Union[datetime.timedelta, float, None]=None) -> datetime.datetime: if not delta: delta = datetime.timedelta(seconds=self.step_width) + elif isinstance(delta, float): + delta = datetime.timedelta(seconds=delta) self.time_to_freeze += delta # type: ignore return self.time_to_freeze From d2872d0afd5b5cce3b6b523bf74a60d0b8191e17 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 6 May 2024 09:07:41 +0200 Subject: [PATCH 2/6] Fix instance checks --- freezegun/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freezegun/api.py b/freezegun/api.py index e48a871..8f0b755 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -532,7 +532,7 @@ def __call__(self) -> datetime.datetime: return self.time_to_freeze def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: - if isinstance(delta, float): + if isinstance(delta, (int, float)): # noinspection PyTypeChecker self.time_to_freeze += datetime.timedelta(seconds=delta) else: @@ -560,7 +560,7 @@ def __call__(self) -> datetime.datetime: def tick(self, delta: Union[datetime.timedelta, float, None]=None) -> datetime.datetime: if not delta: delta = datetime.timedelta(seconds=self.step_width) - elif isinstance(delta, float): + elif isinstance(delta, (int, float)): delta = datetime.timedelta(seconds=delta) self.time_to_freeze += delta # type: ignore return self.time_to_freeze From be779f4b18dc1227cc68775992f3b46c39b2a739 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 6 May 2024 09:08:00 +0200 Subject: [PATCH 3/6] Add test for manually ticking StepTickTimeFactory --- tests/test_operations.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_operations.py b/tests/test_operations.py index d34f3dd..71b94e5 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1,4 +1,5 @@ import datetime +import pytest from freezegun import freeze_time from dateutil.relativedelta import relativedelta from datetime import timedelta, tzinfo @@ -102,3 +103,27 @@ def test_auto_tick() -> None: auto_incremented_time = datetime.datetime.now() assert first_time + datetime.timedelta(seconds=15) == auto_incremented_time + +@pytest.mark.parametrize( + "tick,expected_diff", + ( + (datetime.timedelta(milliseconds=1500), 1.5), + (1, 1), + (1.5, 1.5) + ) +) +def test_auto_and_manual_tick(tick, expected_diff) -> None: + first_time = datetime.datetime(2020, 1, 14, 0, 0, 0, 1) + + with freeze_time(first_time, auto_tick_seconds=2) as frozen_time: + frozen_time.tick(tick) + incremented_time = datetime.datetime.now() + expected_time = first_time + datetime.timedelta(seconds=expected_diff) + assert incremented_time == expected_time + + expected_time += datetime.timedelta(seconds=2) # auto_tick_seconds + + frozen_time.tick(tick) + incremented_time = datetime.datetime.now() + expected_time += datetime.timedelta(seconds=expected_diff) + assert incremented_time == expected_time From 023c7a382fe15ba2f680b43de0e027c460ffc1ed Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 6 May 2024 09:18:35 +0200 Subject: [PATCH 4/6] Revert runtime type-check to `numbers.Real` --- freezegun/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freezegun/api.py b/freezegun/api.py index 8f0b755..4ac68d5 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -509,7 +509,7 @@ def __init__(self, time_to_freeze: datetime.datetime, start: datetime.datetime): def __call__(self) -> datetime.datetime: return self.time_to_freeze + (real_datetime.now() - self.start) - def tick(self, delta: Union[datetime.timedelta, int]=datetime.timedelta(seconds=1)) -> datetime.datetime: + def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: if isinstance(delta, numbers.Real): # noinspection PyTypeChecker self.move_to(self.time_to_freeze + datetime.timedelta(seconds=delta)) @@ -532,7 +532,7 @@ def __call__(self) -> datetime.datetime: return self.time_to_freeze def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: - if isinstance(delta, (int, float)): + if isinstance(delta, numbers.Real): # noinspection PyTypeChecker self.time_to_freeze += datetime.timedelta(seconds=delta) else: @@ -560,7 +560,7 @@ def __call__(self) -> datetime.datetime: def tick(self, delta: Union[datetime.timedelta, float, None]=None) -> datetime.datetime: if not delta: delta = datetime.timedelta(seconds=self.step_width) - elif isinstance(delta, (int, float)): + elif isinstance(delta, numbers.Real): delta = datetime.timedelta(seconds=delta) self.time_to_freeze += delta # type: ignore return self.time_to_freeze From aecc78ad3dfe029ef19003622510b063b3fa6d87 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 6 May 2024 09:42:15 +0200 Subject: [PATCH 5/6] Keep `numbers` for runtime type checks, use `float` for type hints --- freezegun/api.py | 18 +++++++++++------- tests/test_datetimes.py | 12 ++++++++++++ tests/test_operations.py | 17 ++++++++++++++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/freezegun/api.py b/freezegun/api.py index 4ac68d5..d235292 100644 --- a/freezegun/api.py +++ b/freezegun/api.py @@ -510,9 +510,10 @@ def __call__(self) -> datetime.datetime: return self.time_to_freeze + (real_datetime.now() - self.start) def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: - if isinstance(delta, numbers.Real): - # noinspection PyTypeChecker - self.move_to(self.time_to_freeze + datetime.timedelta(seconds=delta)) + if isinstance(delta, numbers.Integral): + self.move_to(self.time_to_freeze + datetime.timedelta(seconds=int(delta))) + elif isinstance(delta, numbers.Real): + self.move_to(self.time_to_freeze + datetime.timedelta(seconds=float(delta))) else: self.move_to(self.time_to_freeze + delta) # type: ignore return self.time_to_freeze @@ -532,9 +533,10 @@ def __call__(self) -> datetime.datetime: return self.time_to_freeze def tick(self, delta: Union[datetime.timedelta, float]=datetime.timedelta(seconds=1)) -> datetime.datetime: - if isinstance(delta, numbers.Real): - # noinspection PyTypeChecker - self.time_to_freeze += datetime.timedelta(seconds=delta) + if isinstance(delta, numbers.Integral): + self.move_to(self.time_to_freeze + datetime.timedelta(seconds=int(delta))) + elif isinstance(delta, numbers.Real): + self.move_to(self.time_to_freeze + datetime.timedelta(seconds=float(delta))) else: self.time_to_freeze += delta # type: ignore return self.time_to_freeze @@ -560,8 +562,10 @@ def __call__(self) -> datetime.datetime: def tick(self, delta: Union[datetime.timedelta, float, None]=None) -> datetime.datetime: if not delta: delta = datetime.timedelta(seconds=self.step_width) + elif isinstance(delta, numbers.Integral): + delta = datetime.timedelta(seconds=int(delta)) elif isinstance(delta, numbers.Real): - delta = datetime.timedelta(seconds=delta) + delta = datetime.timedelta(seconds=float(delta)) self.time_to_freeze += delta # type: ignore return self.time_to_freeze diff --git a/tests/test_datetimes.py b/tests/test_datetimes.py index 12e5949..a6f1989 100644 --- a/tests/test_datetimes.py +++ b/tests/test_datetimes.py @@ -1,6 +1,7 @@ import time import calendar import datetime +import fractions import unittest import locale import sys @@ -180,6 +181,17 @@ def test_manual_increment() -> None: assert frozen_datetime.tick(delta=datetime.timedelta(seconds=10)) == expected assert frozen_datetime() == expected + expected = initial_datetime + datetime.timedelta(seconds=22.5) + ticked_time = frozen_datetime.tick( + delta=fractions.Fraction(3, 2) # type: ignore + # type hints follow the recommendation of + # https://peps.python.org/pep-0484/#the-numeric-tower + # which means for instance `Fraction`s work at runtime, but not + # during static type analysis + ) + assert ticked_time == expected + assert frozen_datetime() == expected + def test_move_to() -> None: initial_datetime = datetime.datetime(year=1, month=7, day=12, diff --git a/tests/test_operations.py b/tests/test_operations.py index 71b94e5..c0b4f37 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -1,10 +1,11 @@ import datetime +import fractions import pytest from freezegun import freeze_time from dateutil.relativedelta import relativedelta from datetime import timedelta, tzinfo from tests import utils -from typing import Any +from typing import Any, Union @freeze_time("2012-01-14") @@ -109,10 +110,20 @@ def test_auto_tick() -> None: ( (datetime.timedelta(milliseconds=1500), 1.5), (1, 1), - (1.5, 1.5) + (1.5, 1.5), + (fractions.Fraction(3, 2), 1.5), ) ) -def test_auto_and_manual_tick(tick, expected_diff) -> None: +def test_auto_and_manual_tick( + tick: Union[ + datetime.timedelta, + float, + # fractions.Fraction, + # Fraction works at runtime, but not at type-checking time + # cf. https://peps.python.org/pep-0484/#the-numeric-tower + ], + expected_diff: float +) -> None: first_time = datetime.datetime(2020, 1, 14, 0, 0, 0, 1) with freeze_time(first_time, auto_tick_seconds=2) as frozen_time: From df263dcec48f43154a5873eb0dff2d4ba94374da Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 6 May 2024 09:45:25 +0200 Subject: [PATCH 6/6] Extend type checking --- tests/test_operations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_operations.py b/tests/test_operations.py index c0b4f37..49b703d 100644 --- a/tests/test_operations.py +++ b/tests/test_operations.py @@ -117,6 +117,7 @@ def test_auto_tick() -> None: def test_auto_and_manual_tick( tick: Union[ datetime.timedelta, + int, float, # fractions.Fraction, # Fraction works at runtime, but not at type-checking time