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