diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bed684aa9..c76723eaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,13 @@ Package maintainers and users who have to manually update their installation may want to subscribe to `GitHub's tag feed `_. +0.14.3 +====== +unreleased +* FIX ``khal new -i`` crashed and discarded the whole event (summary, etc.) + when given an unparseable datetime range, it now reports the problem and + asks again + 0.14.2 ====== unreleased diff --git a/khal/controllers.py b/khal/controllers.py index 6148f29ec..0a22553d9 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -378,9 +378,15 @@ def new_interactive( end_string = info["dtend"].strftime(conf["locale"]["datetimeformat"]) range_string = start_string + " " + end_string daterange = prompt("datetime range", default=range_string) - start, end, allday = parse_datetime.guessrangefstr( - daterange, conf["locale"], adjust_reasonably=True - ) + try: + start, end, allday = parse_datetime.guessrangefstr( + daterange, conf["locale"], adjust_reasonably=True + ) + except DateTimeParseError as error: + # Let the user retry just the datetime range instead of losing + # the whole event. + echo(error) + continue info["dtstart"] = start info["dtend"] = end info["allday"] = allday diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py index 7dc22f276..678415122 100644 --- a/khal/parse_datetime.py +++ b/khal/parse_datetime.py @@ -414,11 +414,14 @@ def guessrangefstr( except (ValueError, DateTimeParseError): pass + example = dt.datetime(2013, 12, 21, 21, 45).strftime(locale["datetimeformat"]) raise DateTimeParseError( - f'Could not parse "{daterange}".\nPlease check your configuration or ' - "run `khal printformats` to see if this does match your configured " - "[long](date|time|datetime)format.\nIf you suspect a bug, please " - "file an issue at https://github.com/pimutils/khal/issues/ " + f'Could not parse "{daterange}" as a daterange.\n' + f'Try formatting dates like this: "{example}".\n' + "Please check your configuration or run `khal printformats` to see " + "if this does match your configured [long](date|time|datetime)format." + "\nIf you suspect a bug, please file an issue at " + "https://github.com/pimutils/khal/issues/ " ) diff --git a/tests/cli_test.py b/tests/cli_test.py index 0f7cbe335..dffd05667 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1144,6 +1144,54 @@ def test_issue_1056(runner): assert result.exit_code == 0 +@freeze_time("2015-6-1 8:00") +def test_issue_789(runner): + """an unparseable datetime range in `khal new -i` shouldn't discard the + whole event and crash, it should report the problem and let the user + retry just the datetime range + """ + runner = runner(print_new="path") + + result = runner.invoke( + main_khal, + ["new", "-i"], + "Coffee w/Somebody\n" + "tuesday 13:30 - 14:30\n" # unparseable, from the original report + "13:00 17:00\n" # retry with a range that does parse + "\n" + "None\n" + "n\n", + ) + assert "Could not parse" in result.output + assert "event saved" in result.output + assert not result.exception + assert result.exit_code == 0 + + +@freeze_time("2015-6-1 8:00") +def test_issue_789_multiple_invalid_attempts(runner): + """the retry from #789 should work for as many bad attempts as the user + makes, not just a single one + """ + runner = runner(print_new="path") + + result = runner.invoke( + main_khal, + ["new", "-i"], + "Coffee w/Somebody\n" + "tuesday 13:30 - 14:30\n" # unparseable + "still not valid input\n" # still unparseable + "13:00 17:00\n" # finally parses + "\n" + "None\n" + "n\n", + ) + assert result.output.count("Could not parse") == 2 + assert "event saved" in result.output + assert not result.exception + assert result.exit_code == 0 + + def test_list_now(runner, tmpdir): # reproduce #693 runner = runner() diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 687ee3c7e..a411c7530 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -1,4 +1,5 @@ import datetime as dt +import re from collections import OrderedDict import pytest @@ -353,6 +354,29 @@ def test_invalid(self): with pytest.raises(DateTimeParseError): guessrangefstr("xxx yyy zzz", locale=LOCALE_BERLIN) + def test_invalid_error_includes_example(self): + """the error for an unparseable daterange should recommend a + formatted example, not just say that the input was rejected + """ + with pytest.raises(DateTimeParseError, match="Try formatting dates like this"): + guessrangefstr("xxx yyy zzz", locale=LOCALE_BERLIN) + + def test_invalid_error_example_matches_locale(self): + """the recommended example should reflect the caller's own + configured datetimeformat, not a fixed string. + """ + # same reference datetime `khal printformats` uses to demonstrate formats + reference = dt.datetime(2013, 12, 21, 21, 45) + + berlin_example = reference.strftime(LOCALE_BERLIN["datetimeformat"]) + with pytest.raises(DateTimeParseError, match=re.escape(berlin_example)): + guessrangefstr("xxx yyy zzz", locale=LOCALE_BERLIN) + + new_york_example = reference.strftime(LOCALE_NEW_YORK["datetimeformat"]) + assert berlin_example != new_york_example + with pytest.raises(DateTimeParseError, match=re.escape(new_york_example)): + guessrangefstr("xxx yyy zzz", locale=LOCALE_NEW_YORK) + @freeze_time("2016-12-30 17:53") def test_short_format_contains_year(self): """if the non long versions of date(time)format contained a year, the