Updates error messages in datetime and makes them consistent between Python and C.
---------
Co-authored-by: Erlend E. Aasland <erlend.aasland@protonmail.com>
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: Paul Ganssle <1377457+pganssle@users.noreply.github.com>
def _days_before_month(year, month):
"year, month -> number of days in year preceding first day of month."
- assert 1 <= month <= 12, 'month must be in 1..12'
+ assert 1 <= month <= 12, f"month must be in 1..12, not {month}"
return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year))
def _ymd2ord(year, month, day):
"year, month, day -> ordinal, considering 01-Jan-0001 as day 1."
- assert 1 <= month <= 12, 'month must be in 1..12'
+ assert 1 <= month <= 12, f"month must be in 1..12, not {month}"
dim = _days_in_month(year, month)
- assert 1 <= day <= dim, ('day must be in 1..%d' % dim)
+ assert 1 <= day <= dim, f"day must be in 1..{dim}, not {day}"
return (_days_before_year(year) +
_days_before_month(year, month) +
day)
def _isoweek_to_gregorian(year, week, day):
# Year is bounded this way because 9999-12-31 is (9999, 52, 5)
if not MINYEAR <= year <= MAXYEAR:
- raise ValueError(f"Year is out of range: {year}")
+ raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")
if not 0 < week < 53:
out_of_range = True
def _check_tzname(name):
if name is not None and not isinstance(name, str):
raise TypeError("tzinfo.tzname() must return None or string, "
- "not '%s'" % type(name))
+ f"not {type(name).__name__!r}")
# name is the offset-producing method, "utcoffset" or "dst".
# offset is what it returned.
if offset is None:
return
if not isinstance(offset, timedelta):
- raise TypeError("tzinfo.%s() must return None "
- "or timedelta, not '%s'" % (name, type(offset)))
+ raise TypeError(f"tzinfo.{name}() must return None "
+ f"or timedelta, not {type(offset).__name__!r}")
if not -timedelta(1) < offset < timedelta(1):
- raise ValueError("%s()=%s, must be strictly between "
- "-timedelta(hours=24) and timedelta(hours=24)" %
- (name, offset))
+ raise ValueError("offset must be a timedelta "
+ "strictly between -timedelta(hours=24) and "
+ f"timedelta(hours=24), not {offset!r}")
def _check_date_fields(year, month, day):
year = _index(year)
month = _index(month)
day = _index(day)
if not MINYEAR <= year <= MAXYEAR:
- raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year)
+ raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")
if not 1 <= month <= 12:
- raise ValueError('month must be in 1..12', month)
+ raise ValueError(f"month must be in 1..12, not {month}")
dim = _days_in_month(year, month)
if not 1 <= day <= dim:
- raise ValueError('day must be in 1..%d' % dim, day)
+ raise ValueError(f"day must be in 1..{dim}, not {day}")
return year, month, day
def _check_time_fields(hour, minute, second, microsecond, fold):
second = _index(second)
microsecond = _index(microsecond)
if not 0 <= hour <= 23:
- raise ValueError('hour must be in 0..23', hour)
+ raise ValueError(f"hour must be in 0..23, not {hour}")
if not 0 <= minute <= 59:
- raise ValueError('minute must be in 0..59', minute)
+ raise ValueError(f"minute must be in 0..59, not {minute}")
if not 0 <= second <= 59:
- raise ValueError('second must be in 0..59', second)
+ raise ValueError(f"second must be in 0..59, not {second}")
if not 0 <= microsecond <= 999999:
- raise ValueError('microsecond must be in 0..999999', microsecond)
+ raise ValueError(f"microsecond must be in 0..999999, not {microsecond}")
if fold not in (0, 1):
- raise ValueError('fold must be either 0 or 1', fold)
+ raise ValueError(f"fold must be either 0 or 1, not {fold}")
return hour, minute, second, microsecond, fold
def _check_tzinfo_arg(tz):
if tz is not None and not isinstance(tz, tzinfo):
- raise TypeError("tzinfo argument must be None or of a tzinfo subclass")
+ raise TypeError(
+ "tzinfo argument must be None or of a tzinfo subclass, "
+ f"not {type(tz).__name__!r}"
+ )
def _divide_and_round(a, b):
"""divide a by b and round result to the nearest integer
if not cls._minoffset <= offset <= cls._maxoffset:
raise ValueError("offset must be a timedelta "
"strictly between -timedelta(hours=24) and "
- "timedelta(hours=24).")
+ f"timedelta(hours=24), not {offset!r}")
return cls._create(offset, name)
def __init_subclass__(cls):
# blow up because other fields are insane.
self.theclass(base[:2] + bytes([ord_byte]) + base[3:])
+ def test_valuerror_messages(self):
+ pattern = re.compile(
+ r"(year|month|day) must be in \d+\.\.\d+, not \d+"
+ )
+ test_cases = [
+ (2009, 1, 32), # Day out of range
+ (2009, 2, 31), # Day out of range
+ (2009, 13, 1), # Month out of range
+ (2009, 0, 1), # Month out of range
+ (10000, 12, 31), # Year out of range
+ (0, 12, 31), # Year out of range
+ ]
+ for case in test_cases:
+ with self.subTest(case):
+ with self.assertRaisesRegex(ValueError, pattern):
+ self.theclass(*case)
+
def test_fromisoformat(self):
# Test that isoformat() is reversible
base_dates = [
self.assertEqual(res.year, 2013)
self.assertEqual(res.fold, fold)
+ def test_valuerror_messages(self):
+ pattern = re.compile(
+ r"(year|month|day|hour|minute|second) must "
+ r"be in \d+\.\.\d+, not \d+"
+ )
+ test_cases = [
+ (2009, 4, 1, 12, 30, 90), # Second out of range
+ (2009, 4, 1, 12, 90, 45), # Minute out of range
+ (2009, 4, 1, 25, 30, 45), # Hour out of range
+ (2009, 4, 32, 24, 0, 0), # Day out of range
+ (2009, 13, 1, 24, 0, 0), # Month out of range
+ (9999, 12, 31, 24, 0, 0), # Year out of range
+ ]
+ for case in test_cases:
+ with self.subTest(case):
+ with self.assertRaisesRegex(ValueError, pattern):
+ self.theclass(*case)
+
def test_fromisoformat_datetime(self):
# Test that isoformat() is reversible
base_dates = [
with self.assertRaises(ValueError):
self.theclass.fromisoformat(bad_str)
+ def test_fromisoformat_fails_datetime_valueerror(self):
+ pattern = re.compile(
+ r"(year|month|day|hour|minute|second) must "
+ r"be in \d+\.\.\d+, not \d+"
+ )
+ bad_strs = [
+ "2009-04-01T12:30:90", # Second out of range
+ "2009-04-01T12:90:45", # Minute out of range
+ "2009-04-01T25:30:45", # Hour out of range
+ "2009-04-32T24:00:00", # Day out of range
+ "2009-13-01T24:00:00", # Month out of range
+ "9999-12-31T24:00:00", # Year out of range
+ ]
+
+ for bad_str in bad_strs:
+ with self.subTest(bad_str=bad_str):
+ with self.assertRaisesRegex(ValueError, pattern):
+ self.theclass.fromisoformat(bad_str)
+
def test_fromisoformat_fails_surrogate(self):
# Test that when fromisoformat() fails with a surrogate character as
# the separator, the error message contains the original string
t2 = t2.replace(tzinfo=Varies())
self.assertTrue(t1 < t2) # t1's offset counter still going up
+ def test_valuerror_messages(self):
+ pattern = re.compile(
+ r"(hour|minute|second|microsecond) must be in \d+\.\.\d+, not \d+"
+ )
+ test_cases = [
+ (12, 30, 90, 9999991), # Microsecond out of range
+ (12, 30, 90, 000000), # Second out of range
+ (25, 30, 45, 000000), # Hour out of range
+ (12, 90, 45, 000000), # Minute out of range
+ ]
+ for case in test_cases:
+ with self.subTest(case):
+ with self.assertRaisesRegex(ValueError, pattern):
+ self.theclass(*case)
+
def test_fromisoformat(self):
time_examples = [
(0, 0, 0, 0),
--- /dev/null
+Added additional information into error messages in :mod:`datetime`, and made the messages more consistent between the C and Python implementations. Patch by Semyon Moroz.
{
if (year < MINYEAR || year > MAXYEAR) {
- PyErr_Format(PyExc_ValueError, "year %i is out of range", year);
+ PyErr_Format(PyExc_ValueError,
+ "year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
return -1;
}
if (month < 1 || month > 12) {
- PyErr_SetString(PyExc_ValueError,
- "month must be in 1..12");
+ PyErr_Format(PyExc_ValueError,
+ "month must be in 1..12, not %d", month);
return -1;
}
- if (day < 1 || day > days_in_month(year, month)) {
- PyErr_SetString(PyExc_ValueError,
- "day is out of range for month");
+ int dim = days_in_month(year, month);
+ if (day < 1 || day > dim) {
+ PyErr_Format(PyExc_ValueError,
+ "day must be in 1..%d, not %d", dim, day);
return -1;
}
return 0;
check_time_args(int h, int m, int s, int us, int fold)
{
if (h < 0 || h > 23) {
- PyErr_SetString(PyExc_ValueError,
- "hour must be in 0..23");
+ PyErr_Format(PyExc_ValueError, "hour must be in 0..23, not %i", h);
return -1;
}
if (m < 0 || m > 59) {
- PyErr_SetString(PyExc_ValueError,
- "minute must be in 0..59");
+ PyErr_Format(PyExc_ValueError, "minute must be in 0..59, not %i", m);
return -1;
}
if (s < 0 || s > 59) {
- PyErr_SetString(PyExc_ValueError,
- "second must be in 0..59");
+ PyErr_Format(PyExc_ValueError, "second must be in 0..59, not %i", s);
return -1;
}
if (us < 0 || us > 999999) {
- PyErr_SetString(PyExc_ValueError,
- "microsecond must be in 0..999999");
+ PyErr_Format(PyExc_ValueError,
+ "microsecond must be in 0..999999, not %i", us);
return -1;
}
if (fold != 0 && fold != 1) {
- PyErr_SetString(PyExc_ValueError,
- "fold must be either 0 or 1");
+ PyErr_Format(PyExc_ValueError,
+ "fold must be either 0 or 1, not %i", fold);
return -1;
}
return 0;
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
" strictly between -timedelta(hours=24) and"
- " timedelta(hours=24),"
- " not %R.", offset);
+ " timedelta(hours=24), not %R", offset);
return NULL;
}
GET_TD_SECONDS(offset) == 0 &&
GET_TD_MICROSECONDS(offset) < 1) ||
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
- Py_DECREF(offset);
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
" strictly between -timedelta(hours=24) and"
- " timedelta(hours=24).");
+ " timedelta(hours=24), not %R", offset);
+ Py_DECREF(offset);
return NULL;
}
}
if (!PyTuple_Check(ratio)) {
PyErr_Format(PyExc_TypeError,
"unexpected return type from as_integer_ratio(): "
- "expected tuple, got '%.200s'",
+ "expected tuple, not '%.200s'",
Py_TYPE(ratio)->tp_name);
Py_DECREF(ratio);
return NULL;
int rv = iso_to_ymd(year, week, day, &year, &month, &day);
if (rv == -4) {
- PyErr_Format(PyExc_ValueError, "Year is out of range: %d", year);
+ PyErr_Format(PyExc_ValueError,
+ "year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
return NULL;
}
}
if (rv == -3) {
- PyErr_Format(PyExc_ValueError, "Invalid day: %d (range is [1, 7])",
+ PyErr_Format(PyExc_ValueError, "Invalid weekday: %d (range is [1, 7])",
day);
return NULL;
}
return NULL;
}
if (!HASTZINFO(dt) || dt->tzinfo != (PyObject *)self) {
- PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo "
- "is not self");
+ PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo is not self");
return NULL;
}
/* ymd_to_ord() doesn't support year <= 0 */
if (year < MINYEAR || year > MAXYEAR) {
- PyErr_Format(PyExc_ValueError, "year %i is out of range", year);
+ PyErr_Format(PyExc_ValueError,
+ "year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
return -1;
}