datetime2 customization

Interface

Base classes of the datetime2 module have all little if no practical use as they natively are. E.g.: even if it stands out for its simplicity, rata die is not a common way of representing dates in the real world.

A mechanism based on attributes, here called “access” attributes, has been implemented to give access to a wide variety of calendars and time representations. A table in the description of each base class lists all currently available access attributes.

The access attribute can be used both on the base class and on a base class instance:

  • if called on the base class it creates a new instance of the base class using the values provided by the interface class:
>>> d1 = Date.gregorian(2013, 4, 18)
>>> d1
datetime2.Date(734976)
>>> t1 = Time.western(17, 16, 28)
>>> t1
datetime2.Time('15547/21600')
  • if called on a base class instance, it allows to see the instance using attributes and methods of the corresponding interface class:
>>> d2 = Date(1)
>>> str(d2.gregorian)
'0001-01-01'
>>> d2.gregorian.month
1
>>> d2.gregorian.weekday()
1
>>> t2 = Time(Fraction(697, 1440))
>>> str(t2.western)
'11:37:00'
>>> t2.western.minute
37

The real power of this paradigm is that we can create a base class instance with an access attribute and see its value with another access attribute, or use different access attributes on the same base class instance. In this way, the base class object is unchanged, but it can bee seen in many different ways.

>>> d = Date.gregorian(2013, 4, 22)
>>> d.iso.week
17
>>> t = Time(0.5)
>>> str(t.western)
'12:00:00'
>>> t.internet.beat
Fraction(500, 1)

A feature of datetime2 is that all representations are computed only once, when first accessed.

Special attention is given when a methods referenced via an access attribute would normally return a new instance: examples are non-default constructors and replace-like methods. When these methods are invoked via an access attribute, the returned value is not an instance of the referenced class, but one of the base class. E.g. GregorianCalendar.replace() returns a GregorianCalendar instance, but when used via the Date class this becomes a Date instance:

>>> d1 = Date.gregorian.year_day(2012, 366)
>>> d1
datetime2.Date(734868)
>>> str(d1.gregorian)
'2012-12-31'
>>> d2 = d1.gregorian.replace(year = 2013, month = 7)
>>> d2
datetime2.Date(735080)
>>> str(d2.gregorian)
'2013-07-31'

As expected, static methods are unchanged even when invoked via access attribute:

>>> Date.gregorian.is_leap_year(2012)
True

Customization

It is possible to add new calendars and/or time representations at run time, by calling a method of the base class and providing the new access attribute name and the new interface class. This new class must satisfy three simple requirements in order to be used as such.

Before examining these requisites in detail, let’s have a look at a simple example: we want to define a new calendar that defines each day by indicating the week number and the week day, counting the week of January 1st of year 1 as week 1 and so on. In addition, this new calendar has a non-default constructor that takes as argument also thousands of weeks:

>>> class SimpleWeekCalendar():
...     def __init__(self, week, day):
...         self.week = week
...         self.day = day
...     @classmethod
...     def from_rata_die(cls, rata_die):
...         return cls((rata_die - 1) // 7 + 1, (rata_die - 1) % 7 + 1)
...     def to_rata_die(self):
...         return 7 * (self.week - 1) + self.day
...     def __str__(self):
...         return 'W{}-{}'.format(self.week, self.day)
...     @classmethod
...     def with_thousands(cls, thousands, week, day):
...         return cls(1000 * thousands + week, day)
...
>>> Date.register_new_calendar('week_count', SimpleWeekCalendar)
>>> d1 = Date.week_count(1, 1)
>>> str(d1.gregorian)
'0001-01-01'
>>> d2 = Date.gregorian(2013, 4, 26)
>>> str(d2.week_count)
'W104998-5'
>>> d3 = Date.week_count.with_thousands(104, 998, 5)
>>> d2 == d3
True

The requirements that must be satisfied to define a custom Date calendar are:

  • The new calendar class must define the non-default constructor from_rata_die, that creates a calendar instance using the day count defined in the Date class as argument.
  • The new calendar class must have the method to_rata_die to convert the given calendar instance to rata die.
  • All other non-default constructors and all methods returning a calendar instance must use the default constructor to return the new calendar instance.

The following tables lists the name required for each base class:

  Date Time DateTime TimeDelta
Registration function register_new_calendar register_new_time TBD TBD
Non-default constructor from_rata_die from_day_frac TBD TBD
Conversion method to_rata_die to_day_frac TBD TBD

All registration methods have the same structure:

registration_method(access_attribute, InterfaceClass)

Register the class InterfaceClass to the corresponding datetime2 base class, accessing it with the access_attribute attribute. If access_attribute is already defined, an AttributeError exception is raised. If access_attribute is not a valid identifier, a ValueError exception is raised.

InterfaceClass must have the non-default constructor and conversion method listed above, otherwise a TypeError exception is raised.

Inner workings

In order to obtain this mechanism, two operations are performed when an interface class is registered to a base class:

  • A new class in created on the fly, so that the new class returns a base class instance when constructor is called, and not an interface class instance. This new class inherits from the interface class, but returns base class instances.
  • A new attribute is added to the base class. This attribute is special: depending on whether it is called on the base class or on a base class instance, it returns or creates the modified interface class instance.

This is an exploit of the standard attribute lookup mechanisms, obtained implementing a context-dependent attribute retrieval, well described in Descriptor HowTo Guide:

  • If the attribute is retrieved directly from the class (e.g. as in Date.week_count(1, 1)), the modified interface class (contained in Date.week_count) is returned, so that when invoked with the interface class signature, it returns a base class instance. The modified interface class was created at registration time, so no additional time is required to create it.
  • If the attribute is retrieved from a base class instance, there are two cases:
    • The instance already has the attribute, which is retrieved normally. Note that this attribute is an instance of the modified interface class, not of the original one.
    • The instance does not have the attribute: the attribute lookup mechanism looks for it in the corresponding Date class definition, where it is found since it was created at registration time. The attribute is created and added to the instance by monkey patching, so the next time it is returned as indicated above.

This quite complex implementation has a few advantages:

  • Base cass instances do not store access attributes unless they are retrieved.
  • Modified interface classes are built at registration time, which happens only once per program invocation.
  • The registration mechanism is common to built-in and custom calendars.
  • Interface classes are completely independent from each other and from their use in base classes.