freezegun icon indicating copy to clipboard operation
freezegun copied to clipboard

host timezone can't be faked

Open IaroslavR opened this issue 6 years ago • 5 comments

fake_tz.py:

from datetime import datetime
import sys

import arrow
import freezegun
from freezegun import freeze_time
import pytz
from tzlocal import get_localzone


def print_dt(tmpl="freeze_time({})"):
    print("Results for: " + tmpl.format(d))
    print("Naive dt", datetime.now())
    print("Local dt: ", get_localzone().localize(datetime.now()))
    print("As arrow obj: ", arrow.now())


print(sys.version)
print(freezegun.__version__, arrow.__version__, pytz.__version__)
print("Real host tz: {}".format(get_localzone()))
d = 'datetime(2015, 8, 18, 10, 00, 00, tzinfo=pytz.timezone("Asia/Kuala_Lumpur"))'
with freeze_time(eval(d)):
    print_dt()
d = 'datetime(2015, 8, 18, 10, 00, 00, tzinfo=arrow.now("Asia/Kuala_Lumpur").tzinfo)'
with freeze_time(eval(d)):
    print_dt()
d = "datetime(2015, 8, 18, 10, 00, 00)"
with freeze_time(eval(d), tz_offset=8):
    print_dt("freeze_time({}, tz_offset=8)")

Output:

$ fake_tz.py
2.7.12 (default, Dec  4 2017, 14:50:18) 
[GCC 5.4.0 20160609]
('0.3.11', '0.13.2', '2019.1')
Real host tz: Europe/Kiev
Results for: freeze_time(datetime(2015, 8, 18, 10, 00, 00, tzinfo=pytz.timezone("Asia/Kuala_Lumpur")))
('Naive dt', FakeDatetime(2015, 8, 18, 3, 13))
('Local dt: ', FakeDatetime(2015, 8, 18, 3, 13, tzinfo=<DstTzInfo 'Europe/Kiev' EEST+3:00:00 DST>))
('As arrow obj: ', <Arrow [2015-08-18T06:13:00+03:00]>)
Results for: freeze_time(datetime(2015, 8, 18, 10, 00, 00, tzinfo=arrow.now("Asia/Kuala_Lumpur").tzinfo))
('Naive dt', FakeDatetime(2015, 8, 18, 2, 0))
('Local dt: ', FakeDatetime(2015, 8, 18, 2, 0, tzinfo=<DstTzInfo 'Europe/Kiev' EEST+3:00:00 DST>))
('As arrow obj: ', <Arrow [2015-08-18T05:00:00+03:00]>)
Results for: freeze_time(datetime(2015, 8, 18, 10, 00, 00), tz_offset=8)
('Naive dt', FakeDatetime(2015, 8, 18, 18, 0))
('Local dt: ', FakeDatetime(2015, 8, 18, 18, 0, tzinfo=<DstTzInfo 'Europe/Kiev' EEST+3:00:00 DST>))
('As arrow obj: ', <Arrow [2015-08-18T21:00:00+03:00]>)
  1. tz from pytz - can't resolve tz for same reason, looks like bug in pytz itself
  2. tz from arrow - tz resolved, naive time correct, tzinfo incorrect(not affected at all)
  3. tz from tz_offset arg - naive time incorrect, tzinfo incorrect(not affected at all)

IaroslavR avatar May 28 '19 03:05 IaroslavR

possible workaround - set env vaiable TZ=Asia/Kuala_Lumpur https://docs.python.org/3/library/time.html#time.tzset

IaroslavR avatar May 28 '19 04:05 IaroslavR

based on @IaroslavR 's comment, I wrote the following context manager which does the job in my testsuite:

@contextlib.contextmanager
def mock_tz(new_tz):
    old_tz = os.environ.get('TZ')
    os.environ['TZ'] = new_tz
    time.tzset()
    try:
        yield
    finally:
        if old_tz is not None:
            os.environ['TZ'] = old_tz
        else:
            del os.environ['TZ']
        time.tzset()

I went with a standalone utility so that I could set the time in any TZ and have my fake system time in a specific TZ. Not sure if this is a common use case. Any how, maybe someone will have a brilliant idea on how to provide a nice API inside freezegun.

Cheers

RemiCardona avatar Oct 31 '19 13:10 RemiCardona

I also think this is a problem. It appears the freezegun devs did this intentionally. There's code in _freeze_time that specifically makes sure to coerce tz-aware datetime objects into a naive form. But for the life of me I can't figure out why they did this. Faking the timezone is part of faking times. Why should a user of this library be prevented from doing this?

bplevin36 avatar Jun 30 '20 16:06 bplevin36

I also believe this is a mistake. I would argue that it's well worth breaking peoples existing tests if they rely on the current behavior in these situations.

boxed avatar Jul 01 '20 12:07 boxed

Relatedly, I believe the freezegun implementation violates the spec of datetime.astimezone, which says:

If called without arguments (or with tz=None) the system local timezone is assumed for the target timezone. The .tzinfo attribute of the converted datetime instance will be set to an instance of timezone with the zone name and offset obtained from the OS.

However, freezegun sets tzinfo to dateutil.tz.tzlocal() which is not an instance of datetime.timezone (and doesn't even have the same methods).

>>> with freeze_time(tz_offset=3):
...     datetime.datetime.now().astimezone()
... 
FakeDatetime(2022, 4, 5, 23, 5, 9, 750369, tzinfo=tzlocal())

Unless I'm missing something?

micahjsmith avatar Apr 05 '22 20:04 micahjsmith