3

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??

Saqib Ali
  • 11,931
  • 41
  • 133
  • 272

1 Answers1

10

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.

SuperShoot
  • 9,880
  • 2
  • 38
  • 55