Patching datetime.date.today() across Python implementations

Altering builtin functions and classes of Python during tests if often frowned upon, but can be actually required while writing unit tests.

The typical builtin requiring such alterations are the datetime.date.today and datetime.datetime.now() functions, which return (obviously) time-dependent values.

We'll take a look at this issue throught the (now standard) mock library.

The problem

The naive approach, mock.patch('datetime.date.today', return_value=MY_NOW), fails:

>>> target = datetime.datetime(2009, 1, 1)
>>> with mock.patch('datetime.datetime.now', return_value=target):
...   print(datetime.datetime.now())
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.2/site-packages/mock.py", line 1359, in __enter__
    setattr(self.target, self.attribute, new_attr)
TypeError: can't set attributes of built-in/extension type 'datetime.datetime'

This error indicates that datetime.date is actually implemented in C, not in Python; it is thus impossible to alter it through setattr

A simple solution

Based on a post by Ned Batchelder, let's try replacing the datetime.datetime class altogether:

>>> target = datetime.datetime(2009, 1, 1)
>>> with mock.patch.object(datetime, 'datetime', mock.Mock(wraps=datetime.datetime)) as patched:
...     patched.now.return_value = target
...     print(datetime.datetime.now())
...
2009-01-01 00:00:00
>>> print(datetime.datetime.now())
2013-04-21 20:37:26

What happens here?

We replace the datetime.datetime class with a mock that simply wraps the class, then alter the return value of the now() class method.

More problems

This solution looks simple, efficient; yet it has another problem: it breaks calls to isinstance(x, datetime.datetime):

>>> target = datetime.datetime(2009, 1, 1)
>>> with mock.patch.object(datetime, 'datetime', mock.Mock(wraps=datetime.datetime)) as patched:
...     isinstance(datetime.datetime.now(), datetime.datetime)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: isinstance() arg 2 must be a type or typle of types

Note

Such isinstance checks are performed in PyPy when adding/substracting datetimes.

The problem comes from having replaced the actual datetime.datetime class with an object (a mock.Mock instance).

A better solution

It appears that the mock library can only produce objects, not classes. We'll have to create our own datetime.datetime subclass:

# mock_dt.py
import datetime
import mock

real_datetime_class = datetime.datetime

def mock_datetime_now(target, dt):
    class DatetimeSubclassMeta(type):
        @classmethod
        def __instancecheck__(mcs, obj):
            return isinstance(obj, real_datetime_class)

    class BaseMockedDatetime(real_datetime_class):
        @classmethod
        def now(cls, tz=None):
            return target.replace(tzinfo=tz)

        @classmethod
        def utcnow(cls):
            return target

    # Python2 & Python3 compatible metaclass
    MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {})

    return mock.patch.object(dt, 'datetime', MockedDatetime)

The process is somewhat complicated, since we're replacing an existing class with one of its subclasses, yet want isinstance checks to pass. This goes through customization of the __instancecheck__ method, that must be defined in a metaclass.

We now have a fully working mock:

>>> target = datetime.datetime(2009, 1, 1)
>>> with mock_datetime_now(target, datetime):
...     print(datetime.datetime.now())
...     print(isinstance(datetime.datetime.now(), datetime.datetime))
...
2009-01-01 00:00:00
True
>>> print(datetime.datetime.now())
2013-04-21 20:37:26

The fully working and documented code can be found here: https://gist.github.com/rbarrois/5430921

Alternate approach

Since finding all calls to today / now in a project's codebase can be quite cumbersome, and they are used massively around, a simpler approach is to write custom functions in a project-side module.

This has been performed by Django through its django.utils.timezone.now() function, that returns datetime.datetime.now localized to the current timezone.

Note

Projects should always use timezone-aware datetimes, defaulting to UTC. This project-wide module would be the perfect place for that.

Once this global module is setup, mocking it becomes much simpler:

jan1 = datetime.datetime(2008, 1, 1, tzinfo=pytz.UTC)

with mock.patch('my_project.time.now', return_value=jan1):
    do_something()