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

Support selective patching #388

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
26 changes: 24 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FreezeGun is a library that allows your Python tests to travel through time by m
Usage
-----

Once the decorator or context manager have been invoked, all calls to datetime.datetime.now(), datetime.datetime.utcnow(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), and time.strftime() will return the time that has been frozen. time.monotonic() will also be frozen, but as usual it makes no guarantees about its absolute value, only its changes over time.
Once the decorator or context manager have been invoked, all calls to datetime.datetime.now(), datetime.datetime.utcnow(), datetime.date.today(), time.time(), time.localtime(), time.gmtime(), and time.strftime() will return the time that has been frozen. time.monotonic() (and time.monotonic_ns()) are not frozen by default and it makes no guarantees about its absolute value, only its changes over time.

Decorator
~~~~~~~~~
Expand Down Expand Up @@ -193,7 +193,7 @@ FreezeGun allows for the time to be manually forwarded as well.
def test_monotonic_manual_tick():
initial_datetime = datetime.datetime(year=1, month=7, day=12,
hour=15, minute=6, second=3)
with freeze_time(initial_datetime) as frozen_datetime:
with freeze_time_with_monotonic(initial_datetime) as frozen_datetime:
monotonic_t0 = time.monotonic()
frozen_datetime.tick(1.0)
monotonic_t1 = time.monotonic()
Expand Down Expand Up @@ -316,3 +316,25 @@ please use:
import freezegun

freezegun.configure(extend_ignore_list=['tensorflow'])


Selective freezing
------------------

By default, `freeze_time` ignores monotonic time freezing to avoid possibly
unexpected behaviour in asynchronous and other settings. To freeze it anyway, one
can use `freeze_time_with_monotonic()` or provide a sequence-like `targets` argument
to `freeze_time()` using the `Target` enum:

.. code-block:: python

from freezegun import freeze_time, freeze_time_with_monotonic, Target, TargetsAll

with freeze_time_with_monotonic('2020-10-06'):
# ...

with freeze_time('2020-10-06', targets=TargetsAll):
# ...

with freeze_time('2020-10-06', targets=(Target.TIME, Target.DATETIME)):
# ...
10 changes: 8 additions & 2 deletions freezegun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
:copyright: (c) 2012 by Steve Pulec.

"""
from .api import freeze_time
from .api import (
freeze_time, freeze_time_with_monotonic, Target, TargetsAll, TargetsDefault,
)
from .config import configure

__title__ = 'freezegun'
Expand All @@ -16,4 +18,8 @@
__copyright__ = 'Copyright 2012 Steve Pulec'


__all__ = ["freeze_time", "configure"]
__all__ = [
"freeze_time", "freeze_time_with_monotonic",
"Target", "TargetsAll", "TargetsDefault",
"configure"
]
111 changes: 72 additions & 39 deletions freezegun/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

from . import config
import dateutil
import datetime
Expand Down Expand Up @@ -539,9 +541,30 @@ def move_to(self, target_datetime):
self.tick(delta=delta)


class Target(str, Enum):
DATE = 'date'
DATETIME = 'datetime'
GMTIME = 'gmtime'
LOCALTIME = 'localtime'
MONOTONIC = 'monotonic'
STRFTIME = 'strftime'
TIME = 'time'
TIME_NS = 'time_ns'
MONOTONIC_NS = 'monotonic_ns'
CLOCK = 'clock'


TargetsDefault = frozenset(
target
for target in Target
if target not in (Target.MONOTONIC, Target.MONOTONIC_NS)
)
TargetsAll = frozenset(Target)


class _freeze_time(object):

def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds):
def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg, auto_tick_seconds, targets):
self.time_to_freeze = _parse_time_to_freeze(time_to_freeze_str)
self.tz_offset = _parse_tz_offset(tz_offset)
self.ignore = tuple(ignore)
Expand All @@ -551,6 +574,7 @@ def __init__(self, time_to_freeze_str, tz_offset, ignore, tick, as_arg, as_kwarg
self.modules_at_start = set()
self.as_arg = as_arg
self.as_kwarg = as_kwarg
self.targets = frozenset(map(Target, targets))

def __call__(self, func):
if inspect.isclass(func):
Expand Down Expand Up @@ -633,46 +657,51 @@ def start(self):
return freeze_factory

# Change the modules
datetime.datetime = FakeDatetime
datetime.date = FakeDate

time.time = fake_time
time.monotonic = fake_monotonic
time.localtime = fake_localtime
time.gmtime = fake_gmtime
time.strftime = fake_strftime
if uuid_generate_time_attr:
setattr(uuid, uuid_generate_time_attr, None)
uuid._UuidCreate = None
uuid._last_timestamp = None

copyreg.dispatch_table[real_datetime] = pickle_fake_datetime
copyreg.dispatch_table[real_date] = pickle_fake_date

# Change any place where the module had already been imported
to_patch = [
('real_date', real_date, FakeDate),
('real_datetime', real_datetime, FakeDatetime),
('real_gmtime', real_gmtime, fake_gmtime),
('real_localtime', real_localtime, fake_localtime),
('real_monotonic', real_monotonic, fake_monotonic),
('real_strftime', real_strftime, fake_strftime),
('real_time', real_time, fake_time),
]

if _TIME_NS_PRESENT:
to_patch = []

if Target.DATETIME in self.targets:
datetime.datetime = FakeDatetime
to_patch.append(('real_datetime', real_datetime, FakeDatetime))
copyreg.dispatch_table[real_datetime] = pickle_fake_datetime
if Target.DATE in self.targets:
datetime.date = FakeDate
to_patch.append(('real_date', real_date, FakeDate))
copyreg.dispatch_table[real_date] = pickle_fake_date

if Target.TIME in self.targets:
time.time = fake_time
to_patch.append(('real_time', real_time, fake_time))
if Target.MONOTONIC in self.targets:
time.monotonic = fake_monotonic
to_patch.append(('real_monotonic', real_monotonic, fake_monotonic))
if Target.LOCALTIME in self.targets:
time.localtime = fake_localtime
to_patch.append(('real_localtime', real_localtime, fake_localtime))
if Target.GMTIME in self.targets:
time.gmtime = fake_gmtime
to_patch.append(('real_gmtime', real_gmtime, fake_gmtime))
if Target.STRFTIME in self.targets:
time.strftime = fake_strftime
to_patch.append(('real_strftime', real_strftime, fake_strftime))

if _TIME_NS_PRESENT and Target.TIME_NS in self.targets:
time.time_ns = fake_time_ns
to_patch.append(('real_time_ns', real_time_ns, fake_time_ns))

if _MONOTONIC_NS_PRESENT:
if _MONOTONIC_NS_PRESENT and Target.MONOTONIC_NS in self.targets:
time.monotonic_ns = fake_monotonic_ns
to_patch.append(('real_monotonic_ns', real_monotonic_ns, fake_monotonic_ns))

if real_clock is not None:
if real_clock is not None and Target.CLOCK in self.targets:
# time.clock is deprecated and was removed in Python 3.8
time.clock = fake_clock
to_patch.append(('real_clock', real_clock, fake_clock))

if uuid_generate_time_attr:
setattr(uuid, uuid_generate_time_attr, None)
uuid._UuidCreate = None
uuid._last_timestamp = None

# Change any place where the module had already been imported

self.fake_names = tuple(fake.__name__ for real_name, real, fake in to_patch)
self.reals = {id(fake): real for real_name, real, fake in to_patch}
fakes = {id(real): fake for real_name, real, fake in to_patch}
Expand Down Expand Up @@ -710,8 +739,8 @@ def stop(self):
if not freeze_factories:
datetime.datetime = real_datetime
datetime.date = real_date
copyreg.dispatch_table.pop(real_datetime)
copyreg.dispatch_table.pop(real_date)
copyreg.dispatch_table.pop(real_datetime, None)
copyreg.dispatch_table.pop(real_date, None)
for module, module_attribute, original_value in self.undo_changes:
setattr(module, module_attribute, original_value)
self.undo_changes = []
Expand Down Expand Up @@ -787,7 +816,7 @@ def wrapper(*args, **kwargs):


def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_arg=False, as_kwarg='',
auto_tick_seconds=0):
auto_tick_seconds=0, targets=TargetsDefault):
acceptable_times = (type(None), _string_type, datetime.date, datetime.timedelta,
types.FunctionType, types.GeneratorType)

Expand All @@ -802,14 +831,14 @@ def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_ar
raise SystemError('Calling freeze_time with tick=True is only compatible with CPython')

if isinstance(time_to_freeze, types.FunctionType):
return freeze_time(time_to_freeze(), tz_offset, ignore, tick, auto_tick_seconds)
return freeze_time(time_to_freeze(), tz_offset, ignore, tick, auto_tick_seconds, targets)

if isinstance(time_to_freeze, types.GeneratorType):
return freeze_time(next(time_to_freeze), tz_offset, ignore, tick, auto_tick_seconds)
return freeze_time(next(time_to_freeze), tz_offset, ignore, tick, auto_tick_seconds, targets)

if MayaDT is not None and isinstance(time_to_freeze, MayaDT):
return freeze_time(time_to_freeze.datetime(), tz_offset, ignore,
tick, as_arg)
tick, as_arg, targets)

if ignore is None:
ignore = []
Expand All @@ -825,9 +854,13 @@ def freeze_time(time_to_freeze=None, tz_offset=0, ignore=None, tick=False, as_ar
as_arg=as_arg,
as_kwarg=as_kwarg,
auto_tick_seconds=auto_tick_seconds,
targets=targets,
)


freeze_time_with_monotonic = functools.partial(freeze_time, targets=TargetsAll)


# Setup adapters for sqlite
try:
# noinspection PyUnresolvedReferences
Expand Down
4 changes: 4 additions & 0 deletions tests/test_configure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest import mock
import freezegun
import freezegun.config
from freezegun import TargetsDefault


def setup_function():
Expand Down Expand Up @@ -31,8 +32,10 @@ def test_default_ignore_list_is_overridden():
as_arg=False,
as_kwarg='',
auto_tick_seconds=0,
targets=TargetsDefault,
)


def test_extend_default_ignore_list():
freezegun.configure(extend_ignore_list=['tensorflow'])

Expand Down Expand Up @@ -62,4 +65,5 @@ def test_extend_default_ignore_list():
as_arg=False,
as_kwarg='',
auto_tick_seconds=0,
targets=TargetsDefault,
)
6 changes: 3 additions & 3 deletions tests/test_datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import pytest
from tests import utils

from freezegun import freeze_time
from freezegun import freeze_time, freeze_time_with_monotonic
from freezegun.api import FakeDatetime, FakeDate

try:
Expand Down Expand Up @@ -205,7 +205,7 @@ def test_bad_time_argument():
def test_time_monotonic():
initial_datetime = datetime.datetime(year=1, month=7, day=12,
hour=15, minute=6, second=3)
with freeze_time(initial_datetime) as frozen_datetime:
with freeze_time_with_monotonic(initial_datetime) as frozen_datetime:
monotonic_t0 = time.monotonic()
if HAS_MONOTONIC_NS:
monotonic_ns_t0 = time.monotonic_ns()
Expand Down Expand Up @@ -680,7 +680,7 @@ def test_time_with_nested():
def test_monotonic_with_nested():
from time import monotonic

with freeze_time('2015-01-01') as frozen_datetime_1:
with freeze_time_with_monotonic('2015-01-01') as frozen_datetime_1:
initial_monotonic_1 = time.monotonic()
with freeze_time('2015-12-25') as frozen_datetime_2:
initial_monotonic_2 = time.monotonic()
Expand Down
81 changes: 81 additions & 0 deletions tests/test_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import datetime
import time

import pytest

from freezegun import freeze_time
from freezegun.api import (
Target, real_date, real_datetime, FakeDate, FakeDatetime, real_gmtime,
fake_gmtime, real_localtime, fake_localtime, fake_monotonic, real_monotonic,
real_strftime, fake_strftime, real_time, fake_time,
freeze_time_with_monotonic,
)

HAS_TIME_NS = hasattr(time, 'time_ns')
HAS_MONOTONIC_NS = hasattr(time, 'monotonic_ns')
HAS_CLOCK = hasattr(time, 'clock')

TARGETS = [
(Target.DATE, datetime, real_date, FakeDate),
(Target.DATETIME, datetime, real_datetime, FakeDatetime),
(Target.GMTIME, time, real_gmtime, fake_gmtime),
(Target.LOCALTIME, time, real_localtime, fake_localtime),
(Target.MONOTONIC, time, real_monotonic, fake_monotonic),
(Target.STRFTIME, time, real_strftime, fake_strftime),
(Target.TIME, time, real_time, fake_time),
]
if HAS_TIME_NS:
from freezegun.api import real_time_ns, fake_time_ns
TARGETS.append((Target.TIME_NS, time, real_time_ns, fake_time_ns))
if HAS_MONOTONIC_NS:
from freezegun.api import real_monotonic_ns, fake_monotonic_ns
TARGETS.append((Target.MONOTONIC_NS, time, real_monotonic_ns, fake_monotonic_ns))
if HAS_CLOCK:
from freezegun.api import real_clock, fake_clock
TARGETS.append((Target.CLOCK, time, real_clock, fake_clock))


@pytest.mark.parametrize(
'target_to_patch,expected',
(
(target, fake)
for (target, _, _, fake) in TARGETS
)
)
def test_target(target_to_patch, expected):
assert TARGETS
with freeze_time(targets={target_to_patch}):
for target, module, real, fake in TARGETS:
assert getattr(module, target) == (
real if target != target_to_patch else fake
)

for target, module, real, fake in TARGETS:
assert getattr(module, target) == real


def test_default_targets():
with freeze_time():
for target, module, real, fake in TARGETS:
assert getattr(module, target) == (
fake
if target not in (Target.MONOTONIC, Target.MONOTONIC_NS)
else real
)

for target, module, real, fake in TARGETS:
assert getattr(module, target) == real


def test_freeze_time_with_monotonic():
with freeze_time_with_monotonic():
for target, module, real, fake in TARGETS:
assert getattr(module, target) == fake

for target, module, real, fake in TARGETS:
assert getattr(module, target) == real


def test_invalid_target():
with pytest.raises(ValueError, match="'invalid' is not a valid Target"):
freeze_time(targets=('invalid',))