Search code examples
pythonsqlalchemyfreezegun

Why doesn't FreezeGun work with SQLAlchemy default values?


I created a SQLAlchemy app with the following model:

class MyObject(db.Model):
    __tablename__ = 'my_object'
    id = db.Column(db.Integer, nullable=False, primary_key=True, autoincrement=True)
    some_string = db.Column(db.String(20), nullable=False)
    created = db.Column(db.DateTime, default=datetime.datetime.now)
    updated = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)

I created this migration file to go along with it:

"""
Junk

Revision ID: 4b6fffffffff_
Revises: 4b6e7775856f
Create Date: 2019-11-08 00:31:13.297355

"""

# revision identifiers, used by Alembic.
revision = '4b6fffffffff_'
down_revision = '4b6e7775856f'

from alembic import op
import sqlalchemy as sa
import sqlalchemy_utils
from sqlalchemy import false


def upgrade():
    op.create_table(
        'my_object',
        sa.Column('created', sa.DateTime(), nullable=False),
        sa.Column('updated', sa.DateTime(), nullable=False),
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('some_string', sa.String(length=20), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )

Now I have this unit-test case which uses Freezegun:

def test_freeze_gun_on_sql_alchemy(self):
    now_time = datetime.datetime(year=2012, month=4, day=1, hour=5, minute=12, second=32, microsecond=543)
    freezer = freeze_time(now_time)
    freezer.start()
    print 'datetime.datetime.now() = {}'.format(datetime.datetime.now())
    m = MyObject(some_string="Hello World")
    db.session.add(m)
    db.session.commit()
    freezer.stop()
    print 'm.created = {}'.format(m.created)

This test case produces this output:

datetime.datetime.now() = 2012-04-01 05:12:32.000543
m.created = 2019-11-09 04:04:55

Why is m.created the current wall-clock time instead of the FreezeGun time?? The two should be the same.

According to this answer, Freezegun patches datetime.datetime.now(). My test case confirms that. So then why/how is a different value being stored in the Database??


Solution

  • As MyObject lives in the module namespace, it, and its class level attributes are evaluated at compile time. This happens before Freezegun has patched datetime.datetime.now, so the column default functions still point to the stdlib implementation.

    Here's a simpler example:

    import datetime
    from freezegun import freeze_time
    
    
    class MyObject:
        dt_now = datetime.datetime.now
    
    
    now_time = datetime.datetime(
        year=2012, month=4, day=1, hour=5, minute=12, second=32, microsecond=543
    )
    freezer = freeze_time(now_time)
    freezer.start()
    print(datetime.datetime.now())  # 2012-04-01 05:12:32.000543
    print(MyObject().dt_now())  # 2019-11-09 15:44:29.437382
    freezer.stop()
    

    Now instantiate Freezegun before setting the class attribute:

    now_time = datetime.datetime(
        year=2012, month=4, day=1, hour=5, minute=12, second=32, microsecond=543
    )
    freezer = freeze_time(now_time)
    freezer.start()
    
    
    class MyObject:
        dt_now = datetime.datetime.now
    
    
    print(datetime.datetime.now())  # 2012-04-01 05:12:32.000543
    print(MyObject().dt_now())  # 2012-04-01 05:12:32.000543
    freezer.stop()
    

    Referencing this issue, wrapping your defaults in a lambda works:

    created = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
    

    ...as that prevents the stdlib function from being bound to the Column default. Or, ensure that the module where MyObject lives is imported after Freezegun is set.