diff --git a/changelog.d/891.bugfix.rst b/changelog.d/891.bugfix.rst new file mode 100644 index 000000000..a5ed17ae4 --- /dev/null +++ b/changelog.d/891.bugfix.rst @@ -0,0 +1,4 @@ +``parser.parse`` will now raise ``TypeError`` when ``tzinfos`` is passed a type +that cannot be interpreted as a time zone. Prior to this change, it would raise +an ``UnboundLocalError`` instead. +Patch by @jbrockmendel (gh pr #891) diff --git a/changelog.d/891.misc.rst b/changelog.d/891.misc.rst new file mode 100644 index 000000000..2e7a44dd0 --- /dev/null +++ b/changelog.d/891.misc.rst @@ -0,0 +1,2 @@ +Added tests for tzinfos input types. +Patch by @jbrockmendel (gh pr #891) diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py index b70e2ca20..2ed091150 100644 --- a/dateutil/parser/_parser.py +++ b/dateutil/parser/_parser.py @@ -1172,6 +1172,9 @@ def _build_tzinfo(self, tzinfos, tzname, tzoffset): tzinfo = tz.tzstr(tzdata) elif isinstance(tzdata, integer_types): tzinfo = tz.tzoffset(tzname, tzdata) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") return tzinfo def _build_tzaware(self, naive, res, tzinfos): diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py index 7dc86751d..bf46dcecd 100644 --- a/dateutil/test/test_parser.py +++ b/dateutil/test/test_parser.py @@ -347,6 +347,62 @@ def test_parse_bytearray(self): assert res == expected +class TestTzinfoInputTypes(object): + def assert_equal_same_tz(self, dt1, dt2): + assert dt1 == dt2 + assert dt1.tzinfo is dt2.tzinfo + + def test_tzinfo_dict_could_return_none(self): + dstr = "2017-02-03 12:40 BRST" + result = parse(dstr, tzinfos={"BRST": None}) + expected = datetime(2017, 2, 3, 12, 40) + self.assert_equal_same_tz(result, expected) + + def test_tzinfos_callable_could_return_none(self): + dstr = "2017-02-03 12:40 BRST" + result = parse(dstr, tzinfos=lambda *args: None) + expected = datetime(2017, 2, 3, 12, 40) + self.assert_equal_same_tz(result, expected) + + def test_invalid_tzinfo_input(self): + dstr = "2014 January 19 09:00 UTC" + # Pass an absurd tzinfos object + tzinfos = {"UTC": ValueError} + with pytest.raises(TypeError): + parse(dstr, tzinfos=tzinfos) + + def test_valid_tzinfo_tzinfo_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {"UTC": tz.UTC} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.UTC) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_unicode_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {u"UTC": u"UTC+0"} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_callable_input(self): + dstr = "2014 January 19 09:00 UTC" + + def tzinfos(*args, **kwargs): + return u"UTC+0" + + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzstr("UTC+0")) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + def test_valid_tzinfo_int_input(self): + dstr = "2014 January 19 09:00 UTC" + tzinfos = {u"UTC": -28800} + expected = datetime(2014, 1, 19, 9, tzinfo=tz.tzoffset(u"UTC", -28800)) + res = parse(dstr, tzinfos=tzinfos) + self.assert_equal_same_tz(res, expected) + + class ParserTest(unittest.TestCase): @classmethod @@ -507,14 +563,6 @@ def testUnspecifiedDayFallbackFebLeapYear(self): self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), datetime(2008, 2, 29)) - def testTzinfoDictionaryCouldReturnNone(self): - self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos={"BRST": None}), - datetime(2017, 2, 3, 12, 40)) - - def testTzinfosCallableCouldReturnNone(self): - self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos=lambda *args: None), - datetime(2017, 2, 3, 12, 40)) - def testErrorType01(self): with pytest.raises(ValueError): parse('shouldfail')