Search code examples
pythondatetimefreezegun

Why does datetime.utcnow not behave as I'd expect with freezegun?


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.


Postscript: time.time doesn't behave this way

Looking 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

Versions in play

$ python --version
Python 3.7.3

$ pip list | grep freezegun
freezegun               0.3.12

Solution

  • 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.