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

parsers: raise ValueError for invalid TimePoints #128

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bandit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
skips: ['B101']
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ python:
- 2.6
- 2.7
env:
- RUN_COVERAGE=false
- RUN_COVERAGE=true
- TZ=UTC

matrix:
fast_finish: true
Expand Down
22 changes: 20 additions & 2 deletions isodatetime/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1425,8 +1425,20 @@ def add_months(self, num_months):
if was_week_date:
self.to_week_date()

def tick_over(self):
"""Correct all the units going from smallest to largest."""
def tick_over(self, check_changes=False):
"""Correct all the units going from smallest to largest.

Args:
check_changes (bool, optional):
If True tick_over will return a dict of any changed fields.

Returns:
dict: Dictionary of changed fields with before and after values
if check_changes is True else None.

"""
if check_changes:
before = {key: getattr(self, key) for key in self.DATA_ATTRIBUTES}
if (self.hour_of_day is not None and
self.minute_of_hour is not None):
hours_remainder = self.hour_of_day - int(self.hour_of_day)
Expand Down Expand Up @@ -1491,6 +1503,12 @@ def tick_over(self):
while self.month_of_year > CALENDAR.MONTHS_IN_YEAR:
self.month_of_year -= CALENDAR.MONTHS_IN_YEAR
self.year += 1
if check_changes:
return {
key: (value, getattr(self, key))
for key, value in before.items()
if getattr(self, key) != value
}

def _tick_over_day_of_month(self):
if self.day_of_month < 1:
Expand Down
45 changes: 39 additions & 6 deletions isodatetime/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,44 @@ def parse_time_zone_expression_to_regex(expression):
expression = "^" + expression + "$"
return expression

def parse(self, timepoint_string, dump_format=None, dump_as_parsed=False):
"""Parse a user-supplied timepoint string."""
def parse(self, timepoint_string, dump_format=None, dump_as_parsed=False,
validate=True):
"""Parse a user-supplied timepoint string.

Args:
validate (bool, optional):
If True the datetime will be "ticked over", if this results in
a change a ValueError will be raised.
Note that `validate` is incompatible with `allow_truncated`.

Raises:
ValueError: If validation fails.

Returns:
TimePoint

"""
date_info, time_info, parsed_expr = self.get_info(timepoint_string)
if dump_as_parsed:
dump_format = parsed_expr
return self._create_timepoint_from_info(
time_point = self._create_timepoint_from_info(
date_info, time_info, dump_format=dump_format,
truncated_dump_format=dump_format)

if validate and not self.allow_truncated:
changed_fields = time_point.tick_over(check_changes=True)
if changed_fields:
raise ValueError(
'Invalid date-time components: ' +
', '.join((
'%s=%s' % (key, before)
for key, (before, after) in changed_fields.items()
if before > after
))
)

return time_point

def _create_timepoint_from_info(self, date_info, time_info,
dump_format=None,
truncated_dump_format=None):
Expand Down Expand Up @@ -574,7 +603,11 @@ def parse(self, expression):
# TimePoint-like duration - don't allow our negative extension.
try:
timepoint = parse_timepoint_expression(
expression[1:], allow_truncated=False,
expression[1:],
# this is a duration but we are parsing it as a timepoint
# so don't validate.
validate=False,
allow_truncated=False,
assumed_time_zone=(0, 0)
)
except ISO8601SyntaxError:
Expand All @@ -597,7 +630,7 @@ def parse(self, expression):
raise ISO8601SyntaxError("duration", expression)


def parse_timepoint_expression(timepoint_expression, **kwargs):
def parse_timepoint_expression(timepoint_expression, validate=True, **kwargs):
"""Return a data model that represents timepoint_expression."""
parser = TimePointParser(**kwargs)
return parser.parse(timepoint_expression)
return parser.parse(timepoint_expression, validate=validate)
42 changes: 42 additions & 0 deletions isodatetime/tests/test_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright (C) 2013-2019 British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------------
"""Test isodatetime.parsers."""

import pytest

from isodatetime.parsers import TimePointParser


def test_invalid_components():
parser = TimePointParser()
for date, invalid in {
'2000-01-01T00:00:60': ['second_of_minute=60'],
'2000-01-01T00:60:00': ['minute_of_hour=60'],
'2000-01-01T60:00:00': ['hour_of_day=60'],
'2000-01-32T00:00:00': ['day_of_month=32'],
'2000-13-00T00:00:00': ['month_of_year=13'],
'2000-13-32T60:60:60': ['month_of_year=13',
'day_of_month=32',
'hour_of_day=60',
'minute_of_hour=60',
'second_of_minute=60']
}.items():
with pytest.raises(ValueError) as exc:
parser.parse(date)
for item in invalid:
assert item in str(exc)