Closed physicalattraction closed 2 months ago
Yeah, you’ve misunderstood it, but the feature isn’t exactly described well. Also the generator support is weird - I copied it from freezegun, but it is confusing, unnecessary, and probably worth deprecating.
time-machine doesn't call next()
on a generator or call a callable between each datetime read. It calls next()
when travel()
starts, like:
times = iter([T1, T2])
with time_machine.travel(times):
# now T1
with time_machine.travel(times):
# now T2
I don’t think it would be particularly easy support your expected behaviour in any way, by having each datetime read call a function or generator. It would be hard to support correct datetime semantics and to remain performant. There can be arbitrary amounts of datetime reads in third-party code.
Hope that clears things up!
Thanks for your answer. I was asking, since I was thinking of using this library instead of our current mock function, which only mocks Django's timezone and doesn't do stuff with timezones and ticks etc. but it does the trick in all our 100s of test cases, including a different time every time it gets called. You are correct that third party code could call any arbitrary number of datetime reads, but that is up to the user of travel()
to handle by giving it a large enough iterable.
This is our implementation. The key difference is that we use side_effect
instead of return_value
when there are multiple values to return. I haven't researched the code in this repository deeply. Would this be an option as well here?
def time_mocked(time) -> patch:
"""
Return a patch for Django's now() function with the given time(s)
:param time: One or more timestamps to return
:return: A patch object for django.utils.timezone.now()
>>> T1 = timezone.datetime(2022,1,2,tzinfo=timezone.utc)
>>> T2 = timezone.datetime(2022,1,3,tzinfo=timezone.utc)
>>> with time_mocked(T1):
... timezone.now()
... timezone.now()
datetime.datetime(2022, 1, 2, 0, 0, tzinfo=<UTC>)
datetime.datetime(2022, 1, 2, 0, 0, tzinfo=<UTC>)
>>> with time_mocked([T1, T2]):
... timezone.now()
... timezone.now()
datetime.datetime(2022, 1, 2, 0, 0, tzinfo=<UTC>)
datetime.datetime(2022, 1, 3, 0, 0, tzinfo=<UTC>)
"""
if isinstance(time, Iterable):
# Return different timestamps every time the function is invoked
return patch.object(timezone, 'now', side_effect=time)
else:
# Return the same timestamp every time the function is invoked
return patch.object(timezone, 'now', return_value=time)
The way that supporting an iterable for travel()
is implemented in freezegun and here as well would also for me not make a lot of sense.
No, I’d rather not support that, I think it will be too error-prone and separates the timestamps from the operations in test code. IMO it’s better to build tests that clearly set the time around expected operations, wrapping travel()
around each operation or using shift()
.
You know the details of this library better than I do, so I trust your judgement. Thanks for your consideration anyway! 🤗
Python Version
3.10.6
pytest Version
No response
Package Version
2.15.0
Description
The documentation mentions the following for the function
travel()
:Additionally, you can provide some more complex types: A generator, in which case next() will be called on it, with the result treated as above. A callable, in which case it will be called with no parameters, with the result treated as above.
However, when I try it, I don't get the desired output:
The real output is the following:
Similar if I try it with a callable, with the same unexpected output:
Is this a misunderstanding on my part about the documentation, and should I use
travel()
differently?