Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix local timezone and timezone related conversions #526

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 91 additions & 45 deletions freezegun/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
import asyncio
import copyreg
import dateutil
import dateutil.tz
import datetime
import functools
import sys
import time
import uuid
import calendar
import unittest
import os
import platform
import warnings
import types
import numbers
import inspect

from dateutil import parser
from dateutil.tz import tzlocal

try:
from maya import MayaDT
Expand All @@ -39,6 +40,8 @@
real_date = datetime.date
real_datetime = datetime.datetime
real_date_objects = [real_time, real_localtime, real_gmtime, real_monotonic, real_perf_counter, real_strftime, real_date, real_datetime]
real_tzlocal = dateutil.tz.tzlocal
real_tz_env = os.environ["TZ"] if "TZ" in os.environ else ''
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
real_tz_env = os.environ["TZ"] if "TZ" in os.environ else ''
real_tz_env = os.environ.get("TZ", "")


if _TIME_NS_PRESENT:
real_time_ns = time.time_ns
Expand Down Expand Up @@ -87,6 +90,9 @@
_GLOBAL_MODULES_CACHE = {}


_tzlocal = real_datetime.now(datetime.UTC).astimezone().tzinfo
_tzlocal_offset = _tzlocal.utcoffset(real_datetime.now(datetime.UTC))

def _get_module_attributes(module):
result = []
try:
Expand Down Expand Up @@ -174,8 +180,16 @@ def get_current_time():
def fake_time():
if _should_use_real_time():
return real_time()
current_time = get_current_time()
return calendar.timegm(current_time.timetuple()) + current_time.microsecond / 1000000.0

current_time_utc = get_current_time() - datetime.timedelta(seconds=tz_offsets[-1].total_seconds())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
current_time_utc = get_current_time() - datetime.timedelta(seconds=tz_offsets[-1].total_seconds())
current_time_utc = get_current_time() - tz_offsets[-1]

tz_offsets contains a list of datetime.timedelta-objects, so this is functionally equivalent as far as I can see


return calendar.timegm(current_time_utc.timetuple()) + current_time_utc.microsecond / 1000000.0

def fake_tzlocal():
if _should_use_real_time():
return real_tzlocal()

return tz_offsets[-1]

if _TIME_NS_PRESENT:
def fake_time_ns():
Expand All @@ -189,16 +203,19 @@ def fake_localtime(t=None):
return real_localtime(t)
if _should_use_real_time():
return real_localtime()
shifted_time = get_current_time() - datetime.timedelta(seconds=time.timezone)
return shifted_time.timetuple()

return get_current_time().timetuple()


def fake_gmtime(t=None):
if t is not None:
return real_gmtime(t)
if _should_use_real_time():
return real_gmtime()
return get_current_time().timetuple()

current_time_utc = get_current_time() - datetime.timedelta(seconds=tz_offsets[-1].total_seconds())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this would always have to be in UTC, wouldn't this be get_current_time() - UTC_offset (whatever the value for UTC_offset is)?


return current_time_utc.timetuple()


def _get_fake_monotonic():
Expand Down Expand Up @@ -256,6 +273,8 @@ def fake_strftime(format, time_to_format=None):
if time_to_format is None:
return real_strftime(format)
else:
# if time_to_format.tzinfo is None:
# time_to_format = time.time(time_to_format).replace(tzinfo=fake_tzlocal())
return real_strftime(format, time_to_format)

if real_clock is not None:
Expand Down Expand Up @@ -323,9 +342,12 @@ def __sub__(self, other):

@classmethod
def today(cls):
result = cls._date_to_freeze() + cls._tz_offset()
result = cls._date_to_freeze()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do need some sort of timezone offset here, as the day can be different in different timezones.

Was there a specific bug with this operation?


return date_to_fakedate(result)



@staticmethod
def _date_to_freeze():
return get_current_time()
Expand Down Expand Up @@ -366,15 +388,17 @@ def __sub__(self, other):

def astimezone(self, tz=None):
if tz is None:
tz = tzlocal()
return datetime_to_fakedatetime(real_datetime.astimezone(self, tz))
tz = self._tz()

if self.tzinfo is None:
return self.replace(tzinfo=tz)
else:
return datetime_to_fakedatetime(real_datetime.astimezone(self, tz))

@classmethod
def fromtimestamp(cls, t, tz=None):
if tz is None:
return real_datetime.fromtimestamp(
t, tz=dateutil.tz.tzoffset("freezegun", cls._tz_offset())
).replace(tzinfo=None)
return real_datetime.fromtimestamp(t, tz=cls._tz()).replace(tzinfo=None)
return datetime_to_fakedatetime(real_datetime.fromtimestamp(t, tz))

def timestamp(self):
Expand All @@ -385,10 +409,11 @@ def timestamp(self):
@classmethod
def now(cls, tz=None):
now = cls._time_to_freeze() or real_datetime.now()
if tz:
result = tz.fromutc(now.replace(tzinfo=tz)) + cls._tz_offset()
if tz is not None:
result = tz.fromutc(now.replace(tzinfo=tz)) - cls._tz_offset()
else:
result = now + cls._tz_offset()
result = now

return datetime_to_fakedatetime(result)

def date(self):
Expand All @@ -408,8 +433,7 @@ def today(cls):

@classmethod
def utcnow(cls):
result = cls._time_to_freeze() or real_datetime.now(datetime.timezone.utc)
return datetime_to_fakedatetime(result)
return cls.now(datetime.UTC).replace(tzinfo=None)

@staticmethod
def _time_to_freeze():
Expand All @@ -420,6 +444,10 @@ def _time_to_freeze():
def _tz_offset(cls):
return tz_offsets[-1]

@classmethod
def _tz(cls):
return dateutil.tz.tzoffset("", tz_offsets[-1])


FakeDatetime.min = datetime_to_fakedatetime(real_datetime.min)
FakeDatetime.max = datetime_to_fakedatetime(real_datetime.max)
Expand All @@ -429,7 +457,7 @@ def convert_to_timezone_naive(time_to_freeze):
"""
Converts a potentially timezone-aware datetime to be a naive UTC datetime
"""
if time_to_freeze.tzinfo:
if time_to_freeze.tzinfo is not None:
time_to_freeze -= time_to_freeze.utcoffset()
time_to_freeze = time_to_freeze.replace(tzinfo=None)
return time_to_freeze
Expand Down Expand Up @@ -458,46 +486,58 @@ def pickle_fake_datetime(datetime_):
)


def _parse_time_to_freeze(time_to_freeze_str):
def _parse_time_to_freeze(time_to_freeze, tz_offset):
"""Parses all the possible inputs for freeze_time
:returns: a naive ``datetime.datetime`` object
"""
if time_to_freeze_str is None:
time_to_freeze_str = datetime.datetime.now(datetime.timezone.utc)

if isinstance(time_to_freeze_str, datetime.datetime):
time_to_freeze = time_to_freeze_str
elif isinstance(time_to_freeze_str, datetime.date):
time_to_freeze = datetime.datetime.combine(time_to_freeze_str, datetime.time())
elif isinstance(time_to_freeze_str, datetime.timedelta):
time_to_freeze = datetime.datetime.now(datetime.timezone.utc) + time_to_freeze_str
if time_to_freeze is None:
time_to_freeze = datetime.datetime.now(datetime.timezone.utc)

if isinstance(time_to_freeze, datetime.datetime):
result = time_to_freeze
elif isinstance(time_to_freeze, datetime.date):
result = datetime.datetime.combine(time_to_freeze, datetime.time())
elif isinstance(time_to_freeze, datetime.timedelta):
result = datetime.datetime.now(datetime.timezone.utc) + time_to_freeze
else:
time_to_freeze = parser.parse(time_to_freeze_str)
result = parser.parse(time_to_freeze)

return convert_to_timezone_naive(time_to_freeze)
if result.tzinfo is None:
return result
else:
return convert_to_timezone_naive(result) + tz_offset


def _parse_tz_offset(tz_offset):
if isinstance(tz_offset, datetime.timedelta):
return tz_offset
else:
elif isinstance(tz_offset, numbers.Real):
return datetime.timedelta(hours=tz_offset)
else:
return _tzlocal_offset


class TickingDateTimeFactory:

def __init__(self, time_to_freeze, start):
def __init__(self, time_to_freeze, tz_offset):
self.time_to_freeze = time_to_freeze
self.start = start
self.start = real_datetime.now()
self.tz_offset = tz_offset

def __call__(self):
return self.time_to_freeze + (real_datetime.now() - self.start)

def move_to(self, target_datetime):
"""Moves frozen date to the given ``target_datetime``"""
self.time_to_freeze = _parse_time_to_freeze(target_datetime, self.tz_offset)
self.start = real_datetime.now()


class FrozenDateTimeFactory:

def __init__(self, time_to_freeze):
def __init__(self, time_to_freeze, tz_offset):
self.time_to_freeze = time_to_freeze
self.tz_offset = tz_offset

def __call__(self):
return self.time_to_freeze
Expand All @@ -511,16 +551,17 @@ def tick(self, delta=datetime.timedelta(seconds=1)):

def move_to(self, target_datetime):
"""Moves frozen date to the given ``target_datetime``"""
target_datetime = _parse_time_to_freeze(target_datetime)
target_datetime = _parse_time_to_freeze(target_datetime, self.tz_offset)
delta = target_datetime - self.time_to_freeze
self.tick(delta=delta)


class StepTickTimeFactory:

def __init__(self, time_to_freeze, step_width):
def __init__(self, time_to_freeze, step_width, tz_offset):
self.time_to_freeze = time_to_freeze
self.step_width = step_width
self.tz_offset = tz_offset

def __call__(self):
return_time = self.time_to_freeze
Expand All @@ -537,16 +578,16 @@ def update_step_width(self, step_width):

def move_to(self, target_datetime):
"""Moves frozen date to the given ``target_datetime``"""
target_datetime = _parse_time_to_freeze(target_datetime)
target_datetime = _parse_time_to_freeze(target_datetime, self.tz_offset)
delta = target_datetime - self.time_to_freeze
self.tick(delta=delta)


class _freeze_time:

def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds):
self.time_to_freeze = _parse_time_to_freeze(time_to_freeze_str)
def __init__(self, time_to_freeze, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds):
self.tz_offset = _parse_tz_offset(tz_offset)
self.time_to_freeze = _parse_time_to_freeze(time_to_freeze, self.tz_offset)
self.ignore = tuple(ignore)
self.tick = tick
self.auto_tick_seconds = auto_tick_seconds
Expand Down Expand Up @@ -637,13 +678,12 @@ def __exit__(self, *args):
self.stop()

def start(self):

if self.auto_tick_seconds:
freeze_factory = StepTickTimeFactory(self.time_to_freeze, self.auto_tick_seconds)
freeze_factory = StepTickTimeFactory(self.time_to_freeze, self.auto_tick_seconds, self.tz_offset)
elif self.tick:
freeze_factory = TickingDateTimeFactory(self.time_to_freeze, real_datetime.now())
freeze_factory = TickingDateTimeFactory(self.time_to_freeze, self.tz_offset)
else:
freeze_factory = FrozenDateTimeFactory(self.time_to_freeze)
freeze_factory = FrozenDateTimeFactory(self.time_to_freeze, self.tz_offset)

is_already_started = len(freeze_factories) > 0
freeze_factories.append(freeze_factory)
Expand All @@ -657,6 +697,7 @@ def start(self):
# Change the modules
datetime.datetime = FakeDatetime
datetime.date = FakeDate
dateutil.tz.tzlocal = fake_tzlocal

time.time = fake_time
time.monotonic = fake_monotonic
Expand All @@ -672,6 +713,8 @@ def start(self):
copyreg.dispatch_table[real_datetime] = pickle_fake_datetime
copyreg.dispatch_table[real_date] = pickle_fake_date

os.environ["TZ"] = "FRZ" + ("-" if self.tz_offset.seconds > 0 else "") + ':'.join(str(self.tz_offset).split(':')[:2])

# Change any place where the module had already been imported
to_patch = [
('real_date', real_date, FakeDate),
Expand All @@ -682,6 +725,7 @@ def start(self):
('real_perf_counter', real_perf_counter, fake_perf_counter),
('real_strftime', real_strftime, fake_strftime),
('real_time', real_time, fake_time),
('real_tzlocal', real_tzlocal, fake_tzlocal),
]

if _TIME_NS_PRESENT:
Expand Down Expand Up @@ -786,13 +830,15 @@ def stop(self):
if real:
setattr(module, module_attribute, real)

dateutil.tz.tzlocal = real_tzlocal
time.time = real_time
time.monotonic = real_monotonic
time.perf_counter = real_perf_counter
time.gmtime = real_gmtime
time.localtime = real_localtime
time.strftime = real_strftime
time.clock = real_clock
os.environ["TZ"] = real_tz_env

if _TIME_NS_PRESENT:
time.time_ns = real_time_ns
Expand Down Expand Up @@ -829,7 +875,7 @@ def wrapper(*args, **kwargs):
return wrapper


def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_arg=False, as_kwarg='',
def freeze_time(time_to_freeze=None, tz_offset=None, ignore=None, tick=False, as_arg=False, as_kwarg='',
auto_tick_seconds=0):
acceptable_times = (type(None), str, datetime.date, datetime.timedelta,
types.FunctionType, types.GeneratorType)
Expand Down Expand Up @@ -861,7 +907,7 @@ def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_ar
ignore.extend(config.settings.default_ignore_list)

return _freeze_time(
time_to_freeze_str=time_to_freeze,
time_to_freeze=time_to_freeze,
tz_offset=tz_offset,
ignore=ignore,
tick=tick,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_datetime_timezone_real():
@freeze_time("2012-01-14 2:00:00", tz_offset=-4)
def test_datetime_timezone_real_with_offset():
now = datetime.datetime.now(tz=GMT5())
assert now == datetime.datetime(2012, 1, 14, 3, tzinfo=GMT5())
assert now == datetime.datetime(2012, 1, 14, 7, tzinfo=GMT5())
assert now.utcoffset() == timedelta(0, 60 * 60 * 5)


Expand Down
Loading