diff --git a/tornado/options.py b/tornado/options.py index 7ebae719f..917c934bd 100644 --- a/tornado/options.py +++ b/tornado/options.py @@ -640,6 +640,8 @@ def _parse_datetime(self, value: str) -> datetime.datetime: ) def _parse_timedelta(self, value: str) -> datetime.timedelta: + if not value or not value.strip(): + raise Error("Invalid time delta: %r" % value) try: sum = datetime.timedelta() start = 0 diff --git a/tornado/test/options_test.py b/tornado/test/options_test.py index 6c76da364..ab7f4b865 100644 --- a/tornado/test/options_test.py +++ b/tornado/test/options_test.py @@ -5,7 +5,7 @@ from io import StringIO from unittest import mock -from tornado.options import Error, OptionParser +from tornado.options import Error, OptionParser, _Option from tornado.util import basestring_type @@ -261,6 +261,20 @@ def test_error_redefine(self): options.define("foo") self.assertRegex(str(cm.exception), "Option.*foo.*already defined") + def test_parse_timedelta_empty_or_whitespace_raises(self): + # Empty or whitespace-only timedelta values used to be silently + # accepted as 0 (the while loop body never ran), so `--t=` and + # `--t=" "` both parsed as datetime.timedelta(0). They now raise + # options.Error with the offending value in the message, matching + # the other type-specific parsers in this module. + for bad in ("", " ", "\t", "\n", " \t "): + option = _Option( + "t", type=datetime.timedelta, default=datetime.timedelta(0) + ) + with self.assertRaises(Error) as cm: + option._parse_timedelta(bad) + self.assertIn(repr(bad), str(cm.exception)) + def test_error_redefine_underscore(self): # Ensure that the dash/underscore normalization doesn't # interfere with the redefinition error.