From 907b36d4351fb28c337ba279f3e88763f4cd01bc Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Fri, 5 Apr 2019 10:21:51 +0100 Subject: [PATCH 1/2] parsers: raise ValueError for invalid TimePoints --- .travis.yml | 3 +-- isodatetime/data.py | 22 ++++++++++++++-- isodatetime/parsers.py | 45 +++++++++++++++++++++++++++----- isodatetime/tests/test_parser.py | 42 +++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 isodatetime/tests/test_parser.py diff --git a/.travis.yml b/.travis.yml index 762986c..fd4cb55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,7 @@ python: - 2.6 - 2.7 env: - - RUN_COVERAGE=false - - RUN_COVERAGE=true + - TZ=UTC matrix: fast_finish: true diff --git a/isodatetime/data.py b/isodatetime/data.py index 6a3c7ec..98dac7a 100644 --- a/isodatetime/data.py +++ b/isodatetime/data.py @@ -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) @@ -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: diff --git a/isodatetime/parsers.py b/isodatetime/parsers.py index 95ef3f5..f0cb5fd 100644 --- a/isodatetime/parsers.py +++ b/isodatetime/parsers.py @@ -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): @@ -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: @@ -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) diff --git a/isodatetime/tests/test_parser.py b/isodatetime/tests/test_parser.py new file mode 100644 index 0000000..6a586b9 --- /dev/null +++ b/isodatetime/tests/test_parser.py @@ -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 . +# ---------------------------------------------------------------------------- +"""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) From f3e8be7a2eae1ac87481eb4f4a50d62b5e965c16 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Thu, 11 Apr 2019 12:37:12 +0100 Subject: [PATCH 2/2] bandit: ignore python assert warnings --- .bandit | 1 + 1 file changed, 1 insertion(+) create mode 100644 .bandit diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..75d550c --- /dev/null +++ b/.bandit @@ -0,0 +1 @@ +skips: ['B101']