I've noticed something which I don't understand, and I'm wondering if anyone could shed some light on it.
In short: if
x = datetime.datetime.utcnow
and
y = lambda: datetime.datetime.utcnow()
I would expect x()
and y()
to behave the same, always. However, this is apparently not the case when freezegun gets involved - it freezes y
but not x
, and I'm wondering why. (This is true if x
and y
are defined outside a freezegun context, anyway; inside such a context, they do behave identically, it seems.)
Example:
from datetime import datetime
import freezegun
# I'd expect these two to behave the same, always.
x = datetime.utcnow
y = lambda: datetime.utcnow()
with freezegun.freeze_time('2019-01-02 03:04:05'):
# Here their behaviours diverge
print('Time from x:', x())
print('Time from y:', y())
# This behaves as I'd expect, however.
z = datetime.utcnow
print('Time from z:', z())))
Results:
Time from x: 2019-10-18 12:21:37.508590
Time from y: 2019-01-02 03:04:05
Time from z: 2019-01-02 03:04:05
Here Time from x
is the time at the moment this is run, i.e it's not under the control of freezegun.
Can anyone shed any light on this? It is just some weirdness of freezegun, or am I misunderstanding something more fundamental about python when I assume that x
and y
should be always equivalent? I see that utcnow
is a bound class method, but I don't get why that would imply this behaviour.
time.time
doesn't behave this wayLooking at utcnow()
's source it's basically just a wrapper around time.time()
— but time.time
and lambda: time.time()
don't diverge in this way... So I'm guessing this does have something to do with utcnow()
being a bound class methiod — but I don't know what.
import time
import freezegun
r = time.time
s = lambda: time.time()
print('Time outside freezegun:', time.time())
with freezegun.freeze_time('2019-01-02 03:04:05'):
print('Time from r:', r())
print('Time from s:', s())
gives:
Time outside freezegun: 1571401765.2612312
Time from r: 1546398245.0
Time from s: 1546398245.0
$ python --version
Python 3.7.3
$ pip list | grep freezegun
freezegun 0.3.12
So I'm guessing this does have something to do with utcnow() being a bound class methiod — but I don't know what.
Looks like your intuition is correct. Freezegun doesn't patch individual methods of datetime
class - instead it replaces the class entirely with its own FakeDatetime
class.
By doing the assignment:
x = datetime.utcnow
x stores reference to the original utcnow()
method, which stays the same even inside freezegun.freeze_time()
context manager.
On the other hand lambda: datetime.utcnow()
calls utcnow()
on datetime
class which is available in current context, and that is FakeDatetime
patched by freezegun.freeze_time()
.
time.time()
is patched by freezegun with fake_time()
. Freezegun even searches through loaded modules and patches variables storing reference to time.time()
, but it's limited to module variables so e.g. it doesn't check inside lists:
import time
import freezegun
r = [time.time]
with freezegun.freeze_time('2019-01-02 03:04:05'):
print('Time inside freezegun:', time.time())
time_inside_list = r[0]
print('Time from list:', time_inside_list())
output:
Time inside freezegun: 1546398245.0
Time from list: 1571669871.8807676
Bonus: If freezegun is so meticulous to find time.time()
references stored in module variables, why doesn't it patch time.time()
used inside of datetime.utcnow()
?
While searching through sys.modules
it deliberately omits datetime
and time
modules to not override the source functions and as a side effect time.time
imported in datetime
module stays not patched.