utils: Add some ISO 8601 forms to get_date()

Support the forms listed by the W3C [1].  I also removed the
'%Y-%d-%m' form, which can be confused with the '%Y-%m-%d' ISO form.
The new ISO forms can use 'Z' to designate UTC or '[+-]HHMM' to
specify offsets from UTC.  Other time zone designators are not
supported.

The '%z' directive has only been supported since Python 3.2 [2], so if
you're running Pelican on Python 2.7, you're stuck with 'Z' for UTC.
Conveniently, we get ValueErrors for both invalid directives and
data/format missmatches, so we don't need special handling for the 2.7
case inside get_date().

[1]: http://www.w3.org/TR/NOTE-datetime
[2]: http://bugs.python.org/issue6641
This commit is contained in:
W. Trevor King 2013-06-11 21:40:13 -04:00
commit 228fc82fc9
3 changed files with 74 additions and 34 deletions

View file

@ -286,7 +286,10 @@ by the directory in which the file resides. For example, a file located at
``python/foobar/myfoobar.rst`` will have a category of ``foobar``. If you would ``python/foobar/myfoobar.rst`` will have a category of ``foobar``. If you would
like to organize your files in other ways where the name of the subfolder would like to organize your files in other ways where the name of the subfolder would
not be a good category name, you can set the setting ``USE_FOLDER_AS_CATEGORY`` not be a good category name, you can set the setting ``USE_FOLDER_AS_CATEGORY``
to ``False``. to ``False``. When parsing dates given in the page metadata, Pelican supports
the W3C's `suggested subset ISO 8601`__.
__ `W3C ISO 8601`_
If you do not explicitly specify summary metadata for a given post, the If you do not explicitly specify summary metadata for a given post, the
``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the ``SUMMARY_MAX_LENGTH`` setting can be used to specify how many words from the
@ -478,3 +481,4 @@ metadata. That article will then be output to the ``drafts`` folder and not
listed on the index page nor on any category page. listed on the index page nor on any category page.
.. _virtualenv: http://www.virtualenv.org/ .. _virtualenv: http://www.virtualenv.org/
.. _W3C ISO 8601: http://www.w3.org/TR/NOTE-datetime

View file

@ -6,7 +6,7 @@ import os
import datetime import datetime
import time import time
import locale import locale
from sys import platform from sys import platform, version_info
from tempfile import mkdtemp from tempfile import mkdtemp
from pelican.generators import TemplatePagesGenerator from pelican.generators import TemplatePagesGenerator
@ -37,26 +37,44 @@ class TestUtils(LoggedTestCase):
def test_get_date(self): def test_get_date(self):
# valid ones # valid ones
date = datetime.datetime(year=2012, month=11, day=22) date = datetime.datetime(year=2012, month=11, day=22)
date_hour = datetime.datetime(year=2012, month=11, day=22, hour=22, date_hour = datetime.datetime(
minute=11) year=2012, month=11, day=22, hour=22, minute=11)
date_hour_sec = datetime.datetime(year=2012, month=11, day=22, hour=22, date_hour_sec = datetime.datetime(
minute=11, second=10) year=2012, month=11, day=22, hour=22, minute=11, second=10)
dates = {'2012-11-22': date, date_hour_sec_z = datetime.datetime(
'2012/11/22': date, year=2012, month=11, day=22, hour=22, minute=11, second=10,
'2012-11-22 22:11': date_hour, tzinfo=datetime.timezone.utc)
'2012/11/22 22:11': date_hour, date_hour_sec_0430 = datetime.datetime(
'22-11-2012': date, year=2012, month=11, day=22, hour=22, minute=11, second=10,
'22/11/2012': date, tzinfo=datetime.timezone(datetime.timedelta(hours=4, minutes=30)))
'22.11.2012': date, date_hour_sec_frac_z = datetime.datetime(
'2012-22-11': date, year=2012, month=11, day=22, hour=22, minute=11, second=10,
'22.11.2012 22:11': date_hour, microsecond=123000, tzinfo=datetime.timezone.utc)
'2012-11-22 22:11:10': date_hour_sec} dates = {
'2012-11-22': date,
'2012/11/22': date,
'2012-11-22 22:11': date_hour,
'2012/11/22 22:11': date_hour,
'22-11-2012': date,
'22/11/2012': date,
'22.11.2012': date,
'22.11.2012 22:11': date_hour,
'2012-11-22 22:11:10': date_hour_sec,
'2012-11-22T22:11:10Z': date_hour_sec_z,
'2012-11-22T22:11:10+0430': date_hour_sec_0430,
'2012-11-22T22:11:10.123Z': date_hour_sec_frac_z,
}
# invalid ones
invalid_dates = ['2010-110-12', 'yay']
if version_info < (3, 2):
dates.pop('2012-11-22T22:11:10-0500')
invalid_dates.append('2012-11-22T22:11:10-0500')
for value, expected in dates.items(): for value, expected in dates.items():
self.assertEqual(utils.get_date(value), expected, value) self.assertEqual(utils.get_date(value), expected, value)
# invalid ones
invalid_dates = ('2010-110-12', 'yay')
for item in invalid_dates: for item in invalid_dates:
self.assertRaises(ValueError, utils.get_date, item) self.assertRaises(ValueError, utils.get_date, item)

View file

@ -15,7 +15,7 @@ from collections import Hashable
from functools import partial from functools import partial
from codecs import open, BOM_UTF8 from codecs import open, BOM_UTF8
from datetime import datetime from datetime import datetime, timezone
from itertools import groupby from itertools import groupby
from jinja2 import Markup from jinja2 import Markup
from operator import attrgetter from operator import attrgetter
@ -180,17 +180,39 @@ def get_date(string):
If no format matches the given date, raise a ValueError. If no format matches the given date, raise a ValueError.
""" """
string = re.sub(' +', ' ', string) string = re.sub(' +', ' ', string)
formats = ['%Y-%m-%d %H:%M', '%Y/%m/%d %H:%M', formats = [
'%Y-%m-%d', '%Y/%m/%d', # ISO 8601
'%d-%m-%Y', '%Y-%d-%m', # Weird ones '%Y',
'%d/%m/%Y', '%d.%m.%Y', '%Y-%m',
'%d.%m.%Y %H:%M', '%Y-%m-%d %H:%M:%S'] '%Y-%m-%d',
'%Y-%m-%dT%H:%M%z',
'%Y-%m-%dT%H:%MZ',
'%Y-%m-%dT%H:%M',
'%Y-%m-%dT%H:%M:%S%z',
'%Y-%m-%dT%H:%M:%SZ',
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%dT%H:%M:%S.%f%z',
'%Y-%m-%dT%H:%M:%S.%fZ',
'%Y-%m-%dT%H:%M:%S.%f',
# end ISO 8601 forms
'%Y-%m-%d %H:%M',
'%Y-%m-%d %H:%M:%S',
'%Y/%m/%d %H:%M',
'%Y/%m/%d',
'%d-%m-%Y',
'%d.%m.%Y %H:%M',
'%d.%m.%Y',
'%d/%m/%Y',
]
for date_format in formats: for date_format in formats:
try: try:
return datetime.strptime(string, date_format) date = datetime.strptime(string, date_format)
except ValueError: except ValueError:
pass continue
raise ValueError("'%s' is not a valid date" % string) if date_format.endswith('Z'):
date = date.replace(tzinfo=timezone.utc)
return date
raise ValueError('{0!r} is not a valid date'.format(string))
class pelican_open(object): class pelican_open(object):
@ -510,15 +532,11 @@ def file_watcher(path):
def set_date_tzinfo(d, tz_name=None): def set_date_tzinfo(d, tz_name=None):
""" Date without tzinfo shoudbe utc. """Set the timezone for dates that don't have tzinfo"""
This function set the right tz to date that aren't utc and don't have if tz_name and not d.tzinfo:
tzinfo.
"""
if tz_name is not None:
tz = pytz.timezone(tz_name) tz = pytz.timezone(tz_name)
return tz.localize(d) return tz.localize(d)
else: return d
return d
def mkdir_p(path): def mkdir_p(path):