Oleh Rybalchenko
Oleh Rybalchenko

Reputation: 8039

Custom datetime subclass that can be created from an existing datetime instance?

I need to have a method to easily create an instance of a datetime.datetime subclass, given an existing datetime.datetime() instance.

Say I have the following contrived example:

class SerializableDateTime(datetime):
    def serialize(self):
        return self.strftime('%Y-%m-%d %H:%M')

I'm using a class like this (but a bit more complex), to use in a SQLAlchemy model; you can tell SQLAlchemy to map a custom class to a supported DateTime column value with a TypeDecorator class; e.g.:

class MyDateTime(types.TypeDecorator):
    impl = types.DateTime

    def process_bind_param(self, value, dialect):
        # from custom type to the SQLAlchemy type compatible with impl
        # a datetime subclass is fine here, no need to convert
        return value

    def process_result_value(self, value, dialect):
        # from SQLAlchemy type to custom type
        # is there a way have this work without accessing a lot of attributes each time?
        return SerializableDateTime(value)   # doesn't work

I can't use return SerializableDateTime(value) here because the default datetime.datetime.__new__() method doesn't accept a datetime.datetime() instance:

>>> value = datetime.now()
>>> SerializableDateTime(value)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type datetime.datetime)

Is there a shortcut that avoids having to copy value.year, value.month, etc. all the way down to the timezone into a constructor?

Upvotes: 3

Views: 656

Answers (1)

Martijn Pieters
Martijn Pieters

Reputation: 1121744

Although you can give your subclass a __new__ method that detects a single datetime.datetime instance then does all the copying there, I'd actually give the class a classmethod just to handle this case, so your SQLAlchemy code would look like:

return SerializableDateTime.from_datetime(value)

We can make use of the pickle support the datetime.datetime() class already implements; types implement the __reduce_ex__ hook (usually building on higher-level methods like __getnewargs__), and for datetime.datetime() instances this hook returns just the datetime.datetime type and an args tuple, meaning that as long as you have a subclass with the same internal state we can create a new copy with the same state by applying the args tuple back to your new type. The __reduce_ex__ method can vary output by pickle protocol, but as long as you pass in pickle.HIGHEST_PROTOCOL you are guaranteed to get the full supported range of values.

The args tuple consists of one or two values, the second being the timezone:

>>> from pickle import HIGHEST_PROTOCOL
>>> value = datetime.now()
>>> value.__reduce_ex__(HIGHEST_PROTOCOL)
(<class 'datetime.datetime'>, (b'\x07\xe2\n\x1f\x12\x06\x05\rd\x8f',))
>>> datetime.utcnow().astimezone(timezone.utc).__reduce_ex__(value.__reduce_ex__(HIGHEST_PROTOCOL))
(<class 'datetime.datetime'>, (b'\x07\xe2\n\x1f\x12\x08\x14\n\xccH', datetime.timezone.utc))

That first value in the args tuple is a bytes value that represents all attributes of the object (excepting the timezone), and the constructor for datetime accepts that same bytes value (plus an optional timezone):

>>> datetime(b'\x07\xe2\n\x1f\x12\x06\x05\rd\x8f') == value
True

Since your subclass accepts the same arguments, you can make use of the args tuple to create a copy; we can use the first value to guard against changes in future Python versions by asserting it's still a parent class of ours:

from pickle import HIGHEST_PROTOCOL

class SerializableDateTime(datetime):
    @classmethod
    def from_datetime(cls, dt):
        """Create a SerializableDateTime instance from a datetime.datetime object"""
        # (ab)use datetime pickle support to copy state across
        factory, args = dt.__reduce_ex__(HIGHEST_PROTOCOL)
        assert issubclass(cls, factory)
        return cls(*args)

    def serialize(self):
        return self.strftime('%Y-%m-%d %H:%M')

This lets you create instances of your subclass as a copy:

>>> SerializableDateTime.from_datetime(datetime.now())
SerializableDateTime(2018, 10, 31, 18, 13, 2, 617875)
>>> SerializableDateTime.from_datetime(datetime.utcnow().astimezone(timezone.utc))
SerializableDateTime(2018, 10, 31, 18, 13, 22, 782185, tzinfo=datetime.timezone.utc)

While using the pickle __reduce_ex__ hook may seem somewhat hackish, this is the actual protocol used to create copies of datetime.datetime instances with the copy module as well, and by using __reduce_ex__(HIGHEST_PROTOCOL) you ensure that all relevant state is copied whatever the Python version you are using.

Upvotes: 5

Related Questions