freezegun
freezegun copied to clipboard
How to freeze datetime for a dataclass field
I'm not sure if I'm doing something wrong here, but this is a dataclass I haven't been able to freeze time for:
import datetime
from dataclasses import dataclass, field
@dataclass
class A:
timestamp: datetime.datetime = field(default_factory=datetime.datetime.now)
Now, I am trying to freeze time using the following pytest
import datetime
from freezegun import freeze_time
@freeze_time("2022-01-14")
def test_A():
a = A()
assert a.timestamp == datetime.datetime(2022,1,14)
This fails because a.timestamp
is not frozen. However, if I change my dataclass like so, it seems to work fine:
@dataclass
class A:
timestamp: Optional[datetime.datetime] = None
def __post_init__():
if self.timestamp is None:
self.timestamp = datetime.datetime.now()
Any suggestions about how to fix this?
The reason this is occurring I believe has to do with the way default_factory
works.
When the python file is first imported, and before freezegun
would have touched it, default_factory
is bound to datetime.datetime.now
. Hence, freezegun
's patch won't catch a usage like that because the default_factory
is already bound. The reason your __post_init__
examples works correctly is because freezegun
will have already patched datetime.datetime.now
at the module level, and the calling code will reference the imported module with freezegun
's changes applied to it.
As for how to fix it, I don't know. You could try using the time-machine library for that use case. It actually has a note in its README on how it solves problems like this, where datetime
-related functions are hidden inside of class level attributes (in this case not exactly, but close enough to use as an example).
Thank you for the insightful response, that definitely helps clear up the confusion! It looks like time-machine fixes the issue I was seeing and I'm able to use field(default_factory=datetime.now)
.
One workaround you could apply is;
@dataclass
class A:
timestamp: datetime.datetime = field(default_factory=lambda: datetime.datetime.now())
By doing this, datetime.datetime.now
is not going to be referenced immediately during module load time. Instead, the one level higher order anonymous lambda
function will be initialized, then whenever an instance is initialized. datetime.now
will be looked up; hence mocking provided by freezegun
will work as expected. I think that is simpler than post_init.
I frequently have the same issue with SQLAlchemy models.
class Model(Base):
...
created_at = Column(DateTime, default=datetime.utcnow)
...
I fix it like this:
def _now():
return datetime.utcnow()
class Model(Base):
...
created_at = Column(DateTime, default=_now)
...