Closed ariebovenberg closed 3 months ago
Having an objective one can explicitly tick is very helpful for getting exact timing results
@RonnyPfannschmidt you mean one that you can explicitly progress?
like this?
with patch_current_time(..., keep_ticking=False) as p:
Instant.now()
p.shift(hours=4)
Instant.now() # shows 4 hours later
Exactly, one common hack is to hijack sleep for progressing time
My impression is that freezegun is the de facto standard for mocking time in python. Maybe it would make sense to just copy the API to make it easier to move between datetime and whenever?
@StefanBRas agree that we should hook into existing libraries like freezegun
where possible. However, instead of copying the entire freezegun API (it's quite large!), we should see if it's possible to plug into it somehow.
freezegun
is explicitly a mock for the datetime module. I would be very surprised if it worked for whenever
.
Also it comes with its own drawbacks (see comparison on yet another datetime mock library).
All these libraries were created because datetime
does not provide a standardized way to mock the time.
I'd start with natively providing a way to mock the time (e.g. like pendulum does) and only if the native way is fundamentally lacking I'd think about using/supporting a 3rd party library.
@StefanBRas agree that we should hook into existing libraries like
freezegun
where possible. However, instead of copying the entire freezegun API (it's quite large!), we should see if it's possible to plug into it somehow.
@ariebovenberg Maybe we're talking past each other here, but the API seems very small to me? It's a single callable used either as a decorator or as as context manager. I'll admit that it takes 8 arguments and I have no idea how much work it is to support the functionality of those.
I wasn't talking about actually using or extending freezegun (Doesn't seem like it has any support for extending except for upstreaming support for whenever) but just copy the API.
freezegun is explicitly a mock for the datetime module. I would be very surprised if it worked for whenever. Also it comes with its own drawbacks (see comparison on yet another datetime mock library).
@spacemanspiff2007 I was only talking about the API, not the implementation. The link actually states that the freezegun
API is clear and good and that the time-machine
API is inspired by it.
I've written some code that uses SystemDateTime
to e.g. run something every day at 2 o'clock.
It would be very nice if it would also be possible to set the timezone for SystemDateTime
.
Currently I have to disable the tests during CI because I can't get them to pass ...
@spacemanspiff2007 setting the local timezone is something that's possible to do in the current version:
>>> import time
>>> import os
>>> os.environ['TZ'] = "Asia/Tokyo"
>>> time.tzset()
>>> from whenever import SystemDateTime
>>> SystemDateTime.now()
SystemDateTime(2024-07-11 19:51:54.762011+09:00)
Note however that tzset
doesn't work on windows, unfortunately...
Java's solution to this problem is the Clock
interface:
clock.instant()
yields the current instant of the clock.clock.getZone()
yields the time-zone being used to create dates and times.Any now()
function optionally accepts a clock, for instance LocalDateTime.now(clock)
@simon04 thanks for sharing. This 'dependency injection' is probably the most reasonable from a proper engineering standpoint and something I'd personally prefer—but there are downsides:
Clock
, requiring discipline and making it impossible to patch any third-party code that simply doesn't do this. Making Clock
required would be too strict.For better or worse, patching is probably the most pragmatic way forward
Note however that
tzset
doesn't work on windows
Additionally it's unclear which side effects tzset has and how performant this is.
Is it suitable for each of the affected test cases to patch it to the desired time zone?
If so then it should be documented under testing in the whenever
docs.
@spacemanspiff2007 I've released a 0.6.4rc0
with patch_current_time
so you can have a run with it—see if it works for your needs.
docs here: https://whenever.readthedocs.io/en/0.6.4rc0/api.html#whenever.patch_current_time
Additionally it's unclear which side effects tzset has and how performant this is. Is it suitable for each of the affected test cases to patch it to the desired time zone?
tzset is documented here. Whenever re-uses Python's datetime logic for the system timezone, so its effects should be exactly the same.
To illustrate, this is how I use tzset()
in the whenever
test suite:
@contextmanager
def system_tz_ams():
if IS_WINDOWS:
pytest.skip("tzset is not available on Windows")
with patch.dict(os.environ, {"TZ": "Europe/Amsterdam"}):
time.tzset()
yield
time.tzset()
Reading up on time-machine
, I've discovered it wouldn't be too hard to make whenever
compatible with it. This means that patch_current_time
can be removed in favor of just using time-machine
.
The only awkwardness is that time-machine
still works with datetime
instances, but you can at least do:
@time_machine.travel("1980-03-02 02:00 UTC")
def test_patch_time():
assert Instant.now() == Instant.from_utc(1980, 3, 2, hour=2)
Potential improvement is to implement a wrapper around time-machine
, or even ask if it can be supported in the library itself.
Using time-machine
can have unintended side effects because it mocks all native datetime/time calls.
This can result in e.g. the asyncio event loop not running properly anymore in case of tick=False
.
I think it's very surprising that wanting to mock whenever
will also change the behavior of e.g. time.monotonic()
The only reason time-machine
exists in the first place is that there is no native way of mocking the standard library.
If your goal is to make time-machine
additionally work with whenever
I'm fine with that but I really think providing a native way makes a lot of sense and is something that is reasonably and also expected by the users.
@spacemanspiff2007 good point! I'll keep the native patch_current_time
and provide time-machine
support for those that want it 👍
Release 0.6.4 is out with the change. I'll leave this issue open for a bit for immediate feedback.
For testing, it's often useful to set the current time in order to control the output of
.now()
.The API would probably look something like this: