ModifiedDate Extension for Sphinx

Updated: Apr 22, 2023

Idea Behind the Extension

When writing article in Hugo [1] you have to provide some date in date field so that Hugo can show it in the statically generated website. Every time you update one article, you have to manually update this field.

I was looking for some way to generate this date automatically in Sphinx [2]. There is a way to add current date using |today| [3] substitution. I was looking for similar kind of substitution which is capable of automatically fetching the current file’s modified date. But unfortunately, I’m not able to find any extension for Sphinx which satisfy my need. So, I decided why not write one?

ModifiedDate Extension

So, I looked into the source code of Sphinx which handles |today| substitution [4], after understanding that code, I started implementing my own extension to Sphinx and came up with my own substitution called |modifieddate|. Here is my extension,

"""modifieddate.

This extension provides `|modifieddate|` substitution

.. moduleauthor:: Mohan R <mohan43u@gmail.com>
"""

import os
import datetime
from docutils import nodes
from docutils.transforms import Transform
from sphinx.util.i18n import format_date
from sphinx.locale import _


class ModifiedDateTransform(Transform):
    """ModifiedDateTransform class."""

    default_priority = 210

    def apply(self, **kwargs):
        """Transform modifieddate substitution."""
        for ref in self.document.traverse(nodes.substitution_reference):
            name = ref['refname']
            if name == 'modifieddate':
                source = self.document['source']
                modifieddate = None
                try:
                    modifieddate = os.stat(source).st_mtime
                except Exception as exception:
                    raise Exception('failed to get modifieddate for %s' % (source)) from exception
                if modifieddate is not None:
                    config = self.document.settings.env.config
                    modifieddate_fmt = config.today_fmt or _('%b %d, %Y')
                    timezone = datetime.timezone.utc
                    modifieddate = datetime.datetime.fromtimestamp(modifieddate,
                                                                   timezone)
                    text = format_date(modifieddate_fmt, modifieddate,
                                       config.language)
                    ref.replace_self(nodes.Text(text, text))


def setup(app):
    """extension."""
    app.add_transform(ModifiedDateTransform)
    return {
        'version': '0.0.1',
        'parallel_read_safe': True,
        'parallel_write_safe': True
    }

Automating Updated Date

Adding |modifieddate| substitution is not enough to automate. Every time I update one article, the current modified date in the disk will change, but I need to preserve the timestamp when I create that article. So I wrote one small script which will look at the prefix of each article filename, If the filename contains a timestamp, then it compares the current modified date for that file in the disk with that timestamp from filename, if they are not equal, then the script will update the modified date to the timestamp from the filename. In this way, I make sure the modified date is always preserved to the timestamp when the article was written. Since the modified date in the disk and the timestamp from the filename got synced, |modifieddate| substitution always return the timestamp from filename as intended.

If there is no timestamp in the filename, then my script will add the current time to the filename, this will make sure the filename of the article contains the original timestamp when it was created.

the date you see at the top of this article after “Updated:” field comes from |modifieddate| substitution

To be continued..