🧊

freezegun で timezone を設定すると時刻がずれる問題と回避策

2024/07/29に公開

次で報告済の内容の解説です:

[Bug] tz_offset shifts datetime which is instantiated with tz argument · Issue #553 · spulec/freezegun

正しい Python の仕様

本来は datetime.now() で生成した時刻は、引数: tz が違っても同じ時刻になります:

from datetime import datetime, timedelta, timezone
from freezegun import freeze_time
from dateutil.tz import tzlocal


def test_freezegun1() -> None:
    timestamp_utc_fixed = int(datetime.now(tz=timezone(timedelta(hours=0))).timestamp() * 1000)
    timestamp_tzlocal = int(datetime.now(tz=tzlocal()).timestamp() * 1000)
    timestamp_local_timezone = int(datetime.now(tz=datetime.now(timezone.utc).astimezone().tzinfo).timestamp() * 1000)
    timestamp_no_tz = int(datetime.now().timestamp() * 1000)
    assert timestamp_utc_fixed == timestamp_no_tz
    assert timestamp_tzlocal == timestamp_no_tz
    assert timestamp_local_timezone == timestamp_no_tz

参考: datetime.datetime.timestamp() の仕様:
datetime --- 基本的な日付と時間の型 — Python 3.12.4 ドキュメント

freezegun の問題の詳細

freeze_time() の引数 tz_offset を与えると、
datetime.now() に引数 tz を与えて生成した日付は、
datetime.now() に引数 tz を与えずに生成した日付に比べ、
tz_offset 時間のズレが発生します:

from datetime import datetime, timedelta, timezone
from freezegun import freeze_time
from dateutil.tz import tzlocal


DIFFERENCE_TIMESTAMP_JST = 9 * 60 * 60 * 1000


@freeze_time("2022-08-09 11:26:00.000", tz_offset=-9)
def test_freezegun2() -> None:
    timestamp_utc_fixed = int(datetime.now(tz=timezone(timedelta(hours=0))).timestamp() * 1000)
    timestamp_tzlocal = int(datetime.now(tz=tzlocal()).timestamp() * 1000)
    timestamp_local_timezone = int(datetime.now(tz=datetime.now(timezone.utc).astimezone().tzinfo).timestamp() * 1000)
    timestamp_no_tz = int(datetime.now().timestamp() * 1000)
    assert timestamp_utc_fixed + DIFFERENCE_TIMESTAMP_JST == timestamp_no_tz  # Wrong!
    assert timestamp_tzlocal + DIFFERENCE_TIMESTAMP_JST == timestamp_no_tz  # Wrong!
    assert timestamp_local_timezone + DIFFERENCE_TIMESTAMP_JST == timestamp_no_tz  # Wrong!

freeze_time() の引数 tz_offset を与えない場合は何も起きません:

from datetime import datetime, timedelta, timezone
from freezegun import freeze_time
from dateutil.tz import tzlocal


@freeze_time("2022-08-09 11:26:00.000")
def test_freezegun3() -> None:
    timestamp_utc_fixed = int(datetime.now(tz=timezone(timedelta(hours=0))).timestamp() * 1000)
    timestamp_tzlocal = int(datetime.now(tz=tzlocal()).timestamp() * 1000)
    timestamp_local_timezone = int(datetime.now(tz=datetime.now(timezone.utc).astimezone().tzinfo).timestamp() * 1000)
    timestamp_no_tz = int(datetime.now().timestamp() * 1000)
    assert timestamp_utc_fixed == timestamp_no_tz
    assert timestamp_tzlocal == timestamp_no_tz
    assert timestamp_local_timezone == timestamp_no_tz

これらのテストは、どのローカルタイムゾーンでも成功するようです

回避策

上記のコードのように、テストコード上で時間のズレを定数定義します
(上記のコードでは DIFFERENCE_TIMESTAMP_JST)

DIFFERENCE_TIMESTAMP_JST = 9 * 60 * 60 * 1000

そして assert などで期待値と実測値の比較をするときに加算または減算します:

@freeze_time("2022-08-09 11:26:00.000", tz_offset=-9)
def test_freezegun4() -> None:
    timestamp_utc_fixed = int(datetime.now(tz=timezone(timedelta(hours=0))).timestamp() * 1000)
    timestamp_no_tz = int(datetime.now().timestamp() * 1000)
    assert timestamp_utc_fixed + DIFFERENCE_TIMESTAMP_JST == timestamp_no_tz

動作確認バージョン

Python: 3.12.4
freezegun: 1.5.1

Discussion