# -*- 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)