Skip to content

Commit

Permalink
Merge pull request #369 from jmehnle/feature/time.monotonic
Browse files Browse the repository at this point in the history
Add support for `time.monotonic` (and `time.monotonic_ns`)
  • Loading branch information
boxed authored Oct 2, 2020
2 parents 7e256a5 + 8938a69 commit b9e15e2
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 3 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Patches and Suggestions
- `James Lu <github.com/CrazyPython>`_
- `Dan Elkis <github.com/rinslow>`_
- `Bastien Vallet <github.com/djailla>`_
- `Julian Mehnle <github.com/jmehnle>`_
16 changes: 14 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.
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.

Decorator
~~~~~~~~~
Expand Down Expand Up @@ -174,7 +174,7 @@ FreezeGun allows for the time to be manually forwarded as well.

.. code-block:: python
def test_manual_increment():
def test_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:
Expand All @@ -188,6 +188,18 @@ FreezeGun allows for the time to be manually forwarded as well.
initial_datetime += datetime.timedelta(seconds=10)
assert frozen_datetime() == initial_datetime
.. code-block:: python
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:
monotonic_t0 = time.monotonic()
frozen_datetime.tick(1.0)
monotonic_t1 = time.monotonic()
assert monotonic_t1 == monotonic_t0 + 1.0
Moving time to specify datetime
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
35 changes: 34 additions & 1 deletion freezegun/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,27 @@
MayaDT = None

_TIME_NS_PRESENT = hasattr(time, 'time_ns')
_MONOTONIC_NS_PRESENT = hasattr(time, 'monotonic_ns')
_EPOCH = datetime.datetime(1970, 1, 1)
_EPOCHTZ = datetime.datetime(1970, 1, 1, tzinfo=dateutil.tz.UTC)

real_time = time.time
real_localtime = time.localtime
real_gmtime = time.gmtime
real_monotonic = time.monotonic
real_strftime = time.strftime
real_date = datetime.date
real_datetime = datetime.datetime
real_date_objects = [real_time, real_localtime, real_gmtime, real_strftime, real_date, real_datetime]
real_date_objects = [real_time, real_localtime, real_gmtime, real_monotonic, real_strftime, real_date, real_datetime]

if _TIME_NS_PRESENT:
real_time_ns = time.time_ns
real_date_objects.append(real_time_ns)

if _MONOTONIC_NS_PRESENT:
real_monotonic_ns = time.monotonic_ns
real_date_objects.append(real_monotonic_ns)

_real_time_object_ids = {id(obj) for obj in real_date_objects}

# time.clock is deprecated and was removed in Python 3.8
Expand Down Expand Up @@ -211,6 +217,23 @@ def fake_gmtime(t=None):
return get_current_time().timetuple()


def fake_monotonic():
if _should_use_real_time():
return real_monotonic()
current_time = get_current_time()
return calendar.timegm(current_time.timetuple()) + current_time.microsecond / 1000000.0

if _MONOTONIC_NS_PRESENT:
def fake_monotonic_ns():
if _should_use_real_time():
return real_monotonic_ns()
current_time = get_current_time()
return (
calendar.timegm(current_time.timetuple()) * 1000000 +
current_time.microsecond
) * 1000


def fake_strftime(format, time_to_format=None):
if time_to_format is None:
if not _should_use_real_time():
Expand Down Expand Up @@ -609,6 +632,7 @@ def start(self):
datetime.date = FakeDate

time.time = fake_time
time.monotonic = fake_monotonic
time.localtime = fake_localtime
time.gmtime = fake_gmtime
time.strftime = fake_strftime
Expand All @@ -626,6 +650,7 @@ def start(self):
('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),
]
Expand All @@ -634,6 +659,10 @@ def start(self):
time.time_ns = fake_time_ns
to_patch.append(('real_time_ns', real_time_ns, fake_time_ns))

if _MONOTONIC_NS_PRESENT:
time.monotonic_ns = fake_monotonic_ns
to_patch.append(('real_monotonic_ns', real_monotonic_ns, fake_monotonic_ns))

if real_clock is not None:
# time.clock is deprecated and was removed in Python 3.8
time.clock = fake_clock
Expand Down Expand Up @@ -710,6 +739,7 @@ def stop(self):
setattr(module, module_attribute, real)

time.time = real_time
time.monotonic = real_monotonic
time.gmtime = real_gmtime
time.localtime = real_localtime
time.strftime = real_strftime
Expand All @@ -718,6 +748,9 @@ def stop(self):
if _TIME_NS_PRESENT:
time.time_ns = real_time_ns

if _MONOTONIC_NS_PRESENT:
time.monotonic_ns = real_monotonic_ns

if uuid_generate_time_attr:
setattr(uuid, uuid_generate_time_attr, real_uuid_generate_time)
uuid._UuidCreate = real_uuid_create
Expand Down
42 changes: 42 additions & 0 deletions tests/test_datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# time.clock was removed in Python 3.8
HAS_CLOCK = hasattr(time, 'clock')
HAS_TIME_NS = hasattr(time, 'time_ns')
HAS_MONOTONIC_NS = hasattr(time, 'monotonic_ns')

class temp_locale(object):
"""Temporarily change the locale."""
Expand Down Expand Up @@ -57,12 +58,14 @@ def test_simple_api():

freezer.start()
assert time.time() == expected_timestamp
assert time.monotonic() >= 0.0
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
assert datetime.datetime.utcnow() == datetime.datetime(2012, 1, 14)
assert datetime.date.today() == datetime.date(2012, 1, 14)
assert datetime.datetime.now().today() == datetime.datetime(2012, 1, 14)
freezer.stop()
assert time.time() != expected_timestamp
assert time.monotonic() >= 0.0
assert datetime.datetime.now() != datetime.datetime(2012, 1, 14)
assert datetime.datetime.utcnow() != datetime.datetime(2012, 1, 14)
freezer = freeze_time("2012-01-10 13:52:01")
Expand Down Expand Up @@ -113,6 +116,7 @@ def test_zero_tz_offset_with_time():
assert datetime.datetime.now() == datetime.datetime(1970, 1, 1)
assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1)
assert time.time() == 0.0
assert time.monotonic() >= 0.0
freezer.stop()


Expand All @@ -125,6 +129,7 @@ def test_tz_offset_with_time():
assert datetime.datetime.now() == datetime.datetime(1969, 12, 31, 20)
assert datetime.datetime.utcnow() == datetime.datetime(1970, 1, 1)
assert time.time() == 0.0
assert time.monotonic() >= 0
freezer.stop()


Expand Down Expand Up @@ -197,6 +202,29 @@ def test_bad_time_argument():
assert False, "Bad values should raise a ValueError"


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:
monotonic_t0 = time.monotonic()
if HAS_MONOTONIC_NS:
monotonic_ns_t0 = time.monotonic_ns()

frozen_datetime.tick()
monotonic_t1 = time.monotonic()
assert monotonic_t1 == monotonic_t0 + 1.0
if HAS_MONOTONIC_NS:
monotonic_ns_t1 = time.monotonic_ns()
assert monotonic_ns_t1 == monotonic_ns_t0 + 1000000000

frozen_datetime.tick(10)
monotonic_t11 = time.monotonic()
assert monotonic_t11 == monotonic_t1 + 10.0
if HAS_MONOTONIC_NS:
monotonic_ns_t11 = time.monotonic_ns()
assert monotonic_ns_t11 == monotonic_ns_t1 + 10000000000


def test_time_gmtime():
with freeze_time('2012-01-14 03:21:34'):
time_struct = time.gmtime()
Expand Down Expand Up @@ -649,6 +677,20 @@ def test_time_with_nested():
assert time() == second


def test_monotonic_with_nested():
from time import monotonic

with freeze_time('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()
frozen_datetime_2.tick()
assert time.monotonic() == initial_monotonic_2 + 1
assert time.monotonic() == initial_monotonic_1
frozen_datetime_1.tick()
assert time.monotonic() == initial_monotonic_1 + 1


def test_should_use_real_time():
frozen = datetime.datetime(2015, 3, 5)
expected_frozen = 1425513600.0
Expand Down
8 changes: 8 additions & 0 deletions tests/test_ticking.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ def test_ticking_time():
assert time.time() > 1326585599.0


@utils.cpython_only
def test_ticking_monotonic():
with freeze_time("Jan 14th, 2012, 23:59:59", tick=True):
initial_monotonic = time.monotonic()
time.sleep(0.001) # Deal with potential clock resolution problems
assert time.monotonic() > initial_monotonic


@mock.patch('freezegun.api._is_cpython', False)
def test_pypy_compat():
try:
Expand Down

0 comments on commit b9e15e2

Please sign in to comment.