blob: ecd6e84a3655663e22c5f7eb09970824cb04a1c7 [file] [log] [blame]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime, timedelta, date, time
import itertools as it
from dateutil.tz import tz
from dateutil.parser import isoparser, isoparse
import pytest
import six
UTC = tz.tzutc()
def _generate_tzoffsets(limited):
def _mkoffset(hmtuple, fmt):
h, m = hmtuple
m_td = (-1 if h < 0 else 1) * m
tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td))
return tzo, fmt.format(h, m)
out = []
if not limited:
# The subset that's just hours
hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)]
out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h])
# Ones that have hours and minutes
hm_out = [] + hm_out_h
hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)]
else:
hm_out = [(-5, -0)]
fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}']
out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts]
# Also add in UTC and naive
out.append((tz.tzutc(), 'Z'))
out.append((None, ''))
return out
FULL_TZOFFSETS = _generate_tzoffsets(False)
FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]]
TZOFFSETS = _generate_tzoffsets(True)
DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)]
@pytest.mark.parametrize('dt', tuple(DATES))
def test_year_only(dt):
dtstr = dt.strftime('%Y')
assert isoparse(dtstr) == dt
DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)]
@pytest.mark.parametrize('dt', tuple(DATES))
def test_year_month(dt):
fmt = '%Y-%m'
dtstr = dt.strftime(fmt)
assert isoparse(dtstr) == dt
DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)]
YMD_FMTS = ('%Y%m%d', '%Y-%m-%d')
@pytest.mark.parametrize('dt', tuple(DATES))
@pytest.mark.parametrize('fmt', YMD_FMTS)
def test_year_month_day(dt, fmt):
dtstr = dt.strftime(fmt)
assert isoparse(dtstr) == dt
def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset,
microsecond_precision=None):
tzi, offset_str = tzoffset
fmt = date_fmt + 'T' + time_fmt
dt = dt.replace(tzinfo=tzi)
dtstr = dt.strftime(fmt)
if microsecond_precision is not None:
if not fmt.endswith('%f'):
raise ValueError('Time format has no microseconds!')
if microsecond_precision != 6:
dtstr = dtstr[:-(6 - microsecond_precision)]
elif microsecond_precision > 6:
raise ValueError('Precision must be 1-6')
dtstr += offset_str
assert isoparse(dtstr) == dt
DATETIMES = [datetime(1998, 4, 16, 12),
datetime(2019, 11, 18, 23),
datetime(2014, 12, 16, 4)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_h(dt, date_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, '%H', tzoffset)
DATETIMES = [datetime(2012, 1, 6, 9, 37)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M'))
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
DATETIMES = [datetime(2003, 9, 2, 22, 14, 2),
datetime(2003, 8, 8, 14, 9, 14),
datetime(2003, 4, 7, 6, 14, 59)]
HMS_FMTS = ('%H%M%S', '%H:%M:%S')
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', HMS_FMTS)
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@pytest.mark.parametrize('date_fmt', YMD_FMTS)
@pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS
for sep in '.,'))
@pytest.mark.parametrize('tzoffset', TZOFFSETS)
@pytest.mark.parametrize('precision', list(range(3, 7)))
def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
# Truncate the microseconds to the desired precision for the representation
dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6)))
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
###
# Truncation of extra digits beyond microsecond precision
@pytest.mark.parametrize('dt_str', [
'2018-07-03T14:07:00.123456000001',
'2018-07-03T14:07:00.123456999999',
])
def test_extra_subsecond_digits(dt_str):
assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456)
@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
def test_full_tzoffsets(tzoffset):
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
date_fmt = '%Y-%m-%d'
time_fmt = '%H:%M:%S.%f'
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
@pytest.mark.parametrize('dt_str', [
'2014-04-11T00',
'2014-04-10T24',
'2014-04-11T00:00',
'2014-04-10T24:00',
'2014-04-11T00:00:00',
'2014-04-10T24:00:00',
'2014-04-11T00:00:00.000',
'2014-04-10T24:00:00.000',
'2014-04-11T00:00:00.000000',
'2014-04-10T24:00:00.000000']
)
def test_datetime_midnight(dt_str):
assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
@pytest.mark.parametrize('datestr', [
'2014-01-01',
'20140101',
])
@pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-'])
def test_isoparse_sep_none(datestr, sep):
isostr = datestr + sep + '14:33:09'
assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9)
##
# Uncommon date formats
TIME_ARGS = ('time_args',
((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz)
for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)],
TZOFFSETS)))
@pytest.mark.parametrize('isocal,dt_expected',[
((2017, 10), datetime(2017, 3, 6)),
((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year
((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014
])
def test_isoweek(isocal, dt_expected):
# TODO: Figure out how to parametrize this on formats, too
for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'):
dtstr = fmt.format(*isocal)
assert isoparse(dtstr) == dt_expected
@pytest.mark.parametrize('isocal,dt_expected',[
((2016, 13, 7), datetime(2016, 4, 3)),
((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year
((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year
((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year
])
def test_isoweek_day(isocal, dt_expected):
# TODO: Figure out how to parametrize this on formats, too
for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'):
dtstr = fmt.format(*isocal)
assert isoparse(dtstr) == dt_expected
@pytest.mark.parametrize('isoord,dt_expected', [
((2004, 1), datetime(2004, 1, 1)),
((2016, 60), datetime(2016, 2, 29)),
((2017, 60), datetime(2017, 3, 1)),
((2016, 366), datetime(2016, 12, 31)),
((2017, 365), datetime(2017, 12, 31))
])
def test_iso_ordinal(isoord, dt_expected):
for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'):
dtstr = fmt.format(*isoord)
assert isoparse(dtstr) == dt_expected
###
# Acceptance of bytes
@pytest.mark.parametrize('isostr,dt', [
(b'2014', datetime(2014, 1, 1)),
(b'20140204', datetime(2014, 2, 4)),
(b'2014-02-04', datetime(2014, 2, 4)),
(b'2014-02-04T12', datetime(2014, 2, 4, 12)),
(b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)),
(b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)),
(b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
(b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
(b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
tz.tzutc())),
(b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000,
tz.tzutc())),
(b'2014-02-04T12:30:15.224+05:00',
datetime(2014, 2, 4, 12, 30, 15, 224000,
tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
def test_bytes(isostr, dt):
assert isoparse(isostr) == dt
###
# Invalid ISO strings
@pytest.mark.parametrize('isostr,exception', [
('201', ValueError), # ISO string too short
('2012-0425', ValueError), # Inconsistent date separators
('201204-25', ValueError), # Inconsistent date separators
('20120425T0120:00', ValueError), # Inconsistent time separators
('20120425T012500-334', ValueError), # Wrong microsecond separator
('2001-1', ValueError), # YYYY-M not valid
('2012-04-9', ValueError), # YYYY-MM-D not valid
('201204', ValueError), # YYYYMM not valid
('20120411T03:30+', ValueError), # Time zone too short
('20120411T03:30+1234567', ValueError), # Time zone too long
('20120411T03:30-25:40', ValueError), # Time zone invalid
('2012-1a', ValueError), # Invalid month
('20120411T03:30+00:60', ValueError), # Time zone invalid minutes
('20120411T03:30+00:61', ValueError), # Time zone invalid minutes
('20120411T033030.123456012:00', # No sign in time zone
ValueError),
('2012-W00', ValueError), # Invalid ISO week
('2012-W55', ValueError), # Invalid ISO week
('2012-W01-0', ValueError), # Invalid ISO week day
('2012-W01-8', ValueError), # Invalid ISO week day
('2013-000', ValueError), # Invalid ordinal day
('2013-366', ValueError), # Invalid ordinal day
('2013366', ValueError), # Invalid ordinal day
('2014-03-12Т12:30:14', ValueError), # Cyrillic T
('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight
('2014_W01-1', ValueError), # Invalid separator
('2014W01-1', ValueError), # Inconsistent use of dashes
('2014-W011', ValueError), # Inconsistent use of dashes
])
def test_iso_raises(isostr, exception):
with pytest.raises(exception):
isoparse(isostr)
@pytest.mark.parametrize('sep_act, valid_sep, exception', [
('T', 'C', ValueError),
('C', 'T', ValueError),
])
def test_iso_with_sep_raises(sep_act, valid_sep, exception):
parser = isoparser(sep=valid_sep)
isostr = '2012-04-25' + sep_act + '01:25:00'
with pytest.raises(exception):
parser.isoparse(isostr)
@pytest.mark.xfail()
@pytest.mark.parametrize('isostr,exception', [
('20120425T01:2000', ValueError), # Inconsistent time separators
])
def test_iso_raises_failing(isostr, exception):
# These are test cases where the current implementation is too lenient
# and need to be fixed
with pytest.raises(exception):
isoparse(isostr)
###
# Test ISOParser constructor
@pytest.mark.parametrize('sep', [' ', '9', '🍛'])
def test_isoparser_invalid_sep(sep):
with pytest.raises(ValueError):
isoparser(sep=sep)
# This only fails on Python 3
@pytest.mark.xfail(six.PY3, reason="Fails on Python 3 only")
def test_isoparser_byte_sep():
dt = datetime(2017, 12, 6, 12, 30, 45)
dt_str = dt.isoformat(sep=str('T'))
dt_rt = isoparser(sep=b'T').isoparse(dt_str)
assert dt == dt_rt
###
# Test parse_tzstr
@pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS)
def test_parse_tzstr(tzoffset):
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
date_fmt = '%Y-%m-%d'
time_fmt = '%H:%M:%S.%f'
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset)
@pytest.mark.parametrize('tzstr', [
'-00:00', '+00:00', '+00', '-00', '+0000', '-0000'
])
@pytest.mark.parametrize('zero_as_utc', [True, False])
def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc):
tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
assert tzi == tz.tzutc()
assert (type(tzi) == tz.tzutc) == zero_as_utc
@pytest.mark.parametrize('tzstr,exception', [
('00:00', ValueError), # No sign
('05:00', ValueError), # No sign
('_00:00', ValueError), # Invalid sign
('+25:00', ValueError), # Offset too large
('00:0000', ValueError), # String too long
])
def test_parse_tzstr_fails(tzstr, exception):
with pytest.raises(exception):
isoparser().parse_tzstr(tzstr)
###
# Test parse_isodate
def __make_date_examples():
dates_no_day = [
date(1999, 12, 1),
date(2016, 2, 1)
]
if six.PY3:
# strftime does not support dates before 1900 in Python 2
dates_no_day.append(date(1000, 11, 1))
# Only one supported format for dates with no day
o = zip(dates_no_day, it.repeat('%Y-%m'))
dates_w_day = [
date(1969, 12, 31),
date(1900, 1, 1),
date(2016, 2, 29),
date(2017, 11, 14)
]
dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d')
o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts))
return list(o)
@pytest.mark.parametrize('d,dt_fmt', __make_date_examples())
@pytest.mark.parametrize('as_bytes', [True, False])
def test_parse_isodate(d, dt_fmt, as_bytes):
d_str = d.strftime(dt_fmt)
if isinstance(d_str, six.text_type) and as_bytes:
d_str = d_str.encode('ascii')
elif isinstance(d_str, bytes) and not as_bytes:
d_str = d_str.decode('ascii')
iparser = isoparser()
assert iparser.parse_isodate(d_str) == d
@pytest.mark.parametrize('isostr,exception', [
('243', ValueError), # ISO string too short
('2014-0423', ValueError), # Inconsistent date separators
('201404-23', ValueError), # Inconsistent date separators
('2014日03月14', ValueError), # Not ASCII
('2013-02-29', ValueError), # Not a leap year
('2014/12/03', ValueError), # Wrong separators
('2014-04-19T', ValueError), # Unknown components
])
def test_isodate_raises(isostr, exception):
with pytest.raises(exception):
isoparser().parse_isodate(isostr)
###
# Test parse_isotime
def __make_time_examples():
outputs = []
# HH
time_h = [time(0), time(8), time(22)]
time_h_fmts = ['%H']
outputs.append(it.product(time_h, time_h_fmts))
# HHMM / HH:MM
time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)]
time_hm_fmts = ['%H%M', '%H:%M']
outputs.append(it.product(time_hm, time_hm_fmts))
# HHMMSS / HH:MM:SS
time_hms = [time(0, 0, 0), time(0, 15, 30),
time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)]
time_hms_fmts = ['%H%M%S', '%H:%M:%S']
outputs.append(it.product(time_hms, time_hms_fmts))
# HHMMSS.ffffff / HH:MM:SS.ffffff
time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993),
time(14, 21, 59, 948730),
time(23, 59, 59, 999999)]
time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f']
outputs.append(it.product(time_hmsu, time_hmsu_fmts))
outputs = list(map(list, outputs))
# Time zones
ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs))
o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr))
o = ((t.replace(tzinfo=tzi), fmt + off_str)
for (t, fmt), (tzi, off_str) in o)
outputs.append(o)
return list(it.chain.from_iterable(outputs))
@pytest.mark.parametrize('time_val,time_fmt', __make_time_examples())
@pytest.mark.parametrize('as_bytes', [True, False])
def test_isotime(time_val, time_fmt, as_bytes):
tstr = time_val.strftime(time_fmt)
if isinstance(time_val, six.text_type) and as_bytes:
tstr = tstr.encode('ascii')
elif isinstance(time_val, bytes) and not as_bytes:
tstr = tstr.decode('ascii')
iparser = isoparser()
assert iparser.parse_isotime(tstr) == time_val
@pytest.mark.parametrize('isostr', [
'24:00',
'2400',
'24:00:00',
'240000',
'24:00:00.000',
'24:00:00,000',
'24:00:00.000000',
'24:00:00,000000',
])
def test_isotime_midnight(isostr):
iparser = isoparser()
assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0)
@pytest.mark.parametrize('isostr,exception', [
('3', ValueError), # ISO string too short
('14時30分15秒', ValueError), # Not ASCII
('14_30_15', ValueError), # Invalid separators
('1430:15', ValueError), # Inconsistent separator use
('25', ValueError), # Invalid hours
('25:15', ValueError), # Invalid hours
('14:60', ValueError), # Invalid minutes
('14:59:61', ValueError), # Invalid seconds
('14:30:15.34468305:00', ValueError), # No sign in time zone
('14:30:15+', ValueError), # Time zone too short
('14:30:15+1234567', ValueError), # Time zone invalid
('14:59:59+25:00', ValueError), # Invalid tz hours
('14:59:59+12:62', ValueError), # Invalid tz minutes
('14:59:30_344583', ValueError), # Invalid microsecond separator
('24:01', ValueError), # 24 used for non-midnight time
('24:00:01', ValueError), # 24 used for non-midnight time
('24:00:00.001', ValueError), # 24 used for non-midnight time
('24:00:00.000001', ValueError), # 24 used for non-midnight time
])
def test_isotime_raises(isostr, exception):
iparser = isoparser()
with pytest.raises(exception):
iparser.parse_isotime(isostr)
@pytest.mark.xfail()
@pytest.mark.parametrize('isostr,exception', [
('14:3015', ValueError), # Inconsistent separator use
('201202', ValueError) # Invalid ISO format
])
def test_isotime_raises_xfail(isostr, exception):
iparser = isoparser()
with pytest.raises(exception):
iparser.parse_isotime(isostr)