Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use custom ParserError class in parser exceptions #881

Merged
merged 4 commits into from Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Expand Up @@ -112,6 +112,7 @@ switch, and thus all their contributions are dual-licensed.
- bachmann <bachmann.matt@MASKED>
- bjv <brandon.vanvaerenbergh@MASKED> (@bjamesvERT)
- gl <gl@MASKED>
- gfyoung <gfyoung17@gmail.com> **D**
- labrys <labrys@MASKED> (gh: @labrys) **R**
- ms-boom <ms-boom@MASKED>
- ryanss <ryanssdev@MASKED> (gh: @ryanss) **R**
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/881.bugfix.rst
@@ -0,0 +1,3 @@
Parsing errors will now raise ``ParserError``, a subclass of ``ValueError``,
which has a nicer string representation.
Patch by @gfyoung (gh pr #881)
3 changes: 2 additions & 1 deletion dateutil/parser/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from ._parser import parse, parser, parserinfo
from ._parser import parse, parser, parserinfo, ParserError
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
from ._parser import UnknownTimezoneWarning

Expand All @@ -9,6 +9,7 @@

__all__ = ['parse', 'parser', 'parserinfo',
'isoparse', 'isoparser',
'ParserError',
'UnknownTimezoneWarning']


Expand Down
26 changes: 21 additions & 5 deletions dateutil/parser/_parser.py
Expand Up @@ -49,7 +49,7 @@
from .. import relativedelta
from .. import tz

__all__ = ["parse", "parserinfo"]
__all__ = ["parse", "parserinfo", "ParserError"]


# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth
Expand Down Expand Up @@ -626,7 +626,7 @@ def parse(self, timestr, default=None,
first element being a :class:`datetime.datetime` object, the second
a tuple containing the fuzzy tokens.

:raises ValueError:
:raises ParserError:
Raised for invalid or unknown string format, if the provided
:class:`tzinfo` is not in a valid format, or if an invalid date
would be created.
Expand All @@ -646,12 +646,15 @@ def parse(self, timestr, default=None,
res, skipped_tokens = self._parse(timestr, **kwargs)

if res is None:
raise ValueError("Unknown string format:", timestr)
raise ParserError("Unknown string format: %s", timestr)

if len(res) == 0:
raise ValueError("String does not contain a date:", timestr)
raise ParserError("String does not contain a date: %s", timestr)

ret = self._build_naive(res, default)
try:
ret = self._build_naive(res, default)
except ValueError as e:
six.raise_from(ParserError(e.args[0] + ": %s", timestr), e)

if not ignoretz:
ret = self._build_tzaware(ret, res, tzinfos)
Expand Down Expand Up @@ -1588,6 +1591,19 @@ def parse(self, tzstr):
def _parsetz(tzstr):
return DEFAULTTZPARSER.parse(tzstr)


class ParserError(ValueError):
"""Error class for representing failure to parse a datetime string."""
def __str__(self):
try:
return self.args[0] % self.args[1:]
except (TypeError, IndexError):
return super(ParserError, self).__str__()

def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, str(self))


class UnknownTimezoneWarning(RuntimeWarning):
"""Raised when the parser finds a timezone it cannot parse into a tzinfo"""
# vim:ts=4:sw=4:et
51 changes: 31 additions & 20 deletions dateutil/test/test_parser.py
Expand Up @@ -9,6 +9,7 @@
from dateutil import tz
from dateutil.tz import tzoffset
from dateutil.parser import parse, parserinfo
from dateutil.parser import ParserError
from dateutil.parser import UnknownTimezoneWarning

from ._common import TZEnvContext
Expand Down Expand Up @@ -291,7 +292,7 @@ def test_strftime_formats_2003Sep25(self, fmt, dstr):

class TestInputTypes(object):
def test_empty_string_invalid(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse('')

def test_none_invalid(self):
Expand Down Expand Up @@ -478,17 +479,17 @@ def testISOStrippedFormatStrip2(self):
tzinfo=tzoffset(None, 10800)))

def testAMPMNoHour(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("AM")

with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("Jan 20, 2015 PM")

def testAMPMRange(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("13:44 AM")

with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("January 25, 1921 23:13 PM")

def testPertain(self):
Expand Down Expand Up @@ -564,15 +565,15 @@ def testUnspecifiedDayFallbackFebLeapYear(self):
datetime(2008, 2, 29))

def testErrorType01(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse('shouldfail')

def testCorrectErrorOnFuzzyWithTokens(self):
assertRaisesRegex(self, ValueError, 'Unknown string format',
assertRaisesRegex(self, ParserError, 'Unknown string format',
parse, '04/04/32/423', fuzzy_with_tokens=True)
assertRaisesRegex(self, ValueError, 'Unknown string format',
assertRaisesRegex(self, ParserError, 'Unknown string format',
parse, '04/04/04 +32423', fuzzy_with_tokens=True)
assertRaisesRegex(self, ValueError, 'Unknown string format',
assertRaisesRegex(self, ParserError, 'Unknown string format',
parse, '04/04/0d4', fuzzy_with_tokens=True)

def testIncreasingCTime(self):
Expand Down Expand Up @@ -713,43 +714,53 @@ def test_hmBY(self):
def test_validate_hour(self):
# See GH353
invalid = "201A-01-01T23:58:39.239769+03:00"
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(invalid)

def test_era_trailing_year(self):
dstr = 'AD2001'
res = parse(dstr)
assert res.year == 2001, res

def test_includes_timestr(self):
timestr = "2020-13-97T44:61:83"

try:
parse(timestr)
except ParserError as e:
assert e.args[1] == timestr
else:
pytest.fail("Failed to raise ParserError")


class TestOutOfBounds(object):

def test_no_year_zero(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("0000 Jun 20")

def test_out_of_bound_day(self):
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse("Feb 30, 2007")

def test_day_sanity(self, fuzzy):
dstr = "2014-15-25"
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(dstr, fuzzy=fuzzy)

def test_minute_sanity(self, fuzzy):
dstr = "2014-02-28 22:64"
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(dstr, fuzzy=fuzzy)

def test_hour_sanity(self, fuzzy):
dstr = "2014-02-28 25:16 PM"
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(dstr, fuzzy=fuzzy)

def test_second_sanity(self, fuzzy):
dstr = "2014-02-28 22:14:64"
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(dstr, fuzzy=fuzzy)


Expand Down Expand Up @@ -802,7 +813,7 @@ def test_four_letter_day(self):
@pytest.mark.xfail
def test_non_date_number(self):
dstr = '1,700'
with pytest.raises(ValueError):
with pytest.raises(ParserError):
parse(dstr)

@pytest.mark.xfail
Expand Down Expand Up @@ -924,7 +935,7 @@ def test_rounding_floatlike_strings(dtstr, dt):

@pytest.mark.parametrize('value', ['1: test', 'Nan'])
def test_decimal_error(value):
# GH 632, GH 662 - decimal.Decimal raises some non-ValueError exception when
# constructed with an invalid value
with pytest.raises(ValueError):
# GH 632, GH 662 - decimal.Decimal raises some non-ParserError exception
# when constructed with an invalid value
with pytest.raises(ParserError):
parse(value)