Skip to content

Commit 3e222e3

Browse files
donBarboserlend-aaslandZeroIntensitypganssle
authored
gh-109798: Normalize _datetime and datetime error messages (#127345)
Updates error messages in datetime and makes them consistent between Python and C. --------- Co-authored-by: Erlend E. Aasland <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Paul Ganssle <[email protected]>
1 parent 57f45ee commit 3e222e3

File tree

4 files changed

+118
-46
lines changed

4 files changed

+118
-46
lines changed

Diff for: Lib/_pydatetime.py

+23-20
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ def _days_in_month(year, month):
6060

6161
def _days_before_month(year, month):
6262
"year, month -> number of days in year preceding first day of month."
63-
assert 1 <= month <= 12, 'month must be in 1..12'
63+
assert 1 <= month <= 12, f"month must be in 1..12, not {month}"
6464
return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year))
6565

6666
def _ymd2ord(year, month, day):
6767
"year, month, day -> ordinal, considering 01-Jan-0001 as day 1."
68-
assert 1 <= month <= 12, 'month must be in 1..12'
68+
assert 1 <= month <= 12, f"month must be in 1..12, not {month}"
6969
dim = _days_in_month(year, month)
70-
assert 1 <= day <= dim, ('day must be in 1..%d' % dim)
70+
assert 1 <= day <= dim, f"day must be in 1..{dim}, not {day}"
7171
return (_days_before_year(year) +
7272
_days_before_month(year, month) +
7373
day)
@@ -512,7 +512,7 @@ def _parse_isoformat_time(tstr):
512512
def _isoweek_to_gregorian(year, week, day):
513513
# Year is bounded this way because 9999-12-31 is (9999, 52, 5)
514514
if not MINYEAR <= year <= MAXYEAR:
515-
raise ValueError(f"Year is out of range: {year}")
515+
raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")
516516

517517
if not 0 < week < 53:
518518
out_of_range = True
@@ -545,7 +545,7 @@ def _isoweek_to_gregorian(year, week, day):
545545
def _check_tzname(name):
546546
if name is not None and not isinstance(name, str):
547547
raise TypeError("tzinfo.tzname() must return None or string, "
548-
"not '%s'" % type(name))
548+
f"not {type(name).__name__!r}")
549549

550550
# name is the offset-producing method, "utcoffset" or "dst".
551551
# offset is what it returned.
@@ -558,24 +558,24 @@ def _check_utc_offset(name, offset):
558558
if offset is None:
559559
return
560560
if not isinstance(offset, timedelta):
561-
raise TypeError("tzinfo.%s() must return None "
562-
"or timedelta, not '%s'" % (name, type(offset)))
561+
raise TypeError(f"tzinfo.{name}() must return None "
562+
f"or timedelta, not {type(offset).__name__!r}")
563563
if not -timedelta(1) < offset < timedelta(1):
564-
raise ValueError("%s()=%s, must be strictly between "
565-
"-timedelta(hours=24) and timedelta(hours=24)" %
566-
(name, offset))
564+
raise ValueError("offset must be a timedelta "
565+
"strictly between -timedelta(hours=24) and "
566+
f"timedelta(hours=24), not {offset!r}")
567567

568568
def _check_date_fields(year, month, day):
569569
year = _index(year)
570570
month = _index(month)
571571
day = _index(day)
572572
if not MINYEAR <= year <= MAXYEAR:
573-
raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year)
573+
raise ValueError(f"year must be in {MINYEAR}..{MAXYEAR}, not {year}")
574574
if not 1 <= month <= 12:
575-
raise ValueError('month must be in 1..12', month)
575+
raise ValueError(f"month must be in 1..12, not {month}")
576576
dim = _days_in_month(year, month)
577577
if not 1 <= day <= dim:
578-
raise ValueError('day must be in 1..%d' % dim, day)
578+
raise ValueError(f"day must be in 1..{dim}, not {day}")
579579
return year, month, day
580580

581581
def _check_time_fields(hour, minute, second, microsecond, fold):
@@ -584,20 +584,23 @@ def _check_time_fields(hour, minute, second, microsecond, fold):
584584
second = _index(second)
585585
microsecond = _index(microsecond)
586586
if not 0 <= hour <= 23:
587-
raise ValueError('hour must be in 0..23', hour)
587+
raise ValueError(f"hour must be in 0..23, not {hour}")
588588
if not 0 <= minute <= 59:
589-
raise ValueError('minute must be in 0..59', minute)
589+
raise ValueError(f"minute must be in 0..59, not {minute}")
590590
if not 0 <= second <= 59:
591-
raise ValueError('second must be in 0..59', second)
591+
raise ValueError(f"second must be in 0..59, not {second}")
592592
if not 0 <= microsecond <= 999999:
593-
raise ValueError('microsecond must be in 0..999999', microsecond)
593+
raise ValueError(f"microsecond must be in 0..999999, not {microsecond}")
594594
if fold not in (0, 1):
595-
raise ValueError('fold must be either 0 or 1', fold)
595+
raise ValueError(f"fold must be either 0 or 1, not {fold}")
596596
return hour, minute, second, microsecond, fold
597597

598598
def _check_tzinfo_arg(tz):
599599
if tz is not None and not isinstance(tz, tzinfo):
600-
raise TypeError("tzinfo argument must be None or of a tzinfo subclass")
600+
raise TypeError(
601+
"tzinfo argument must be None or of a tzinfo subclass, "
602+
f"not {type(tz).__name__!r}"
603+
)
601604

602605
def _divide_and_round(a, b):
603606
"""divide a by b and round result to the nearest integer
@@ -2418,7 +2421,7 @@ def __new__(cls, offset, name=_Omitted):
24182421
if not cls._minoffset <= offset <= cls._maxoffset:
24192422
raise ValueError("offset must be a timedelta "
24202423
"strictly between -timedelta(hours=24) and "
2421-
"timedelta(hours=24).")
2424+
f"timedelta(hours=24), not {offset!r}")
24222425
return cls._create(offset, name)
24232426

24242427
def __init_subclass__(cls):

Diff for: Lib/test/datetimetester.py

+69
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,23 @@ def test_backdoor_resistance(self):
19621962
# blow up because other fields are insane.
19631963
self.theclass(base[:2] + bytes([ord_byte]) + base[3:])
19641964

1965+
def test_valuerror_messages(self):
1966+
pattern = re.compile(
1967+
r"(year|month|day) must be in \d+\.\.\d+, not \d+"
1968+
)
1969+
test_cases = [
1970+
(2009, 1, 32), # Day out of range
1971+
(2009, 2, 31), # Day out of range
1972+
(2009, 13, 1), # Month out of range
1973+
(2009, 0, 1), # Month out of range
1974+
(10000, 12, 31), # Year out of range
1975+
(0, 12, 31), # Year out of range
1976+
]
1977+
for case in test_cases:
1978+
with self.subTest(case):
1979+
with self.assertRaisesRegex(ValueError, pattern):
1980+
self.theclass(*case)
1981+
19651982
def test_fromisoformat(self):
19661983
# Test that isoformat() is reversible
19671984
base_dates = [
@@ -3212,6 +3229,24 @@ class DateTimeSubclass(self.theclass):
32123229
self.assertEqual(res.year, 2013)
32133230
self.assertEqual(res.fold, fold)
32143231

3232+
def test_valuerror_messages(self):
3233+
pattern = re.compile(
3234+
r"(year|month|day|hour|minute|second) must "
3235+
r"be in \d+\.\.\d+, not \d+"
3236+
)
3237+
test_cases = [
3238+
(2009, 4, 1, 12, 30, 90), # Second out of range
3239+
(2009, 4, 1, 12, 90, 45), # Minute out of range
3240+
(2009, 4, 1, 25, 30, 45), # Hour out of range
3241+
(2009, 4, 32, 24, 0, 0), # Day out of range
3242+
(2009, 13, 1, 24, 0, 0), # Month out of range
3243+
(9999, 12, 31, 24, 0, 0), # Year out of range
3244+
]
3245+
for case in test_cases:
3246+
with self.subTest(case):
3247+
with self.assertRaisesRegex(ValueError, pattern):
3248+
self.theclass(*case)
3249+
32153250
def test_fromisoformat_datetime(self):
32163251
# Test that isoformat() is reversible
32173252
base_dates = [
@@ -3505,6 +3540,25 @@ def test_fromisoformat_fails_datetime(self):
35053540
with self.assertRaises(ValueError):
35063541
self.theclass.fromisoformat(bad_str)
35073542

3543+
def test_fromisoformat_fails_datetime_valueerror(self):
3544+
pattern = re.compile(
3545+
r"(year|month|day|hour|minute|second) must "
3546+
r"be in \d+\.\.\d+, not \d+"
3547+
)
3548+
bad_strs = [
3549+
"2009-04-01T12:30:90", # Second out of range
3550+
"2009-04-01T12:90:45", # Minute out of range
3551+
"2009-04-01T25:30:45", # Hour out of range
3552+
"2009-04-32T24:00:00", # Day out of range
3553+
"2009-13-01T24:00:00", # Month out of range
3554+
"9999-12-31T24:00:00", # Year out of range
3555+
]
3556+
3557+
for bad_str in bad_strs:
3558+
with self.subTest(bad_str=bad_str):
3559+
with self.assertRaisesRegex(ValueError, pattern):
3560+
self.theclass.fromisoformat(bad_str)
3561+
35083562
def test_fromisoformat_fails_surrogate(self):
35093563
# Test that when fromisoformat() fails with a surrogate character as
35103564
# the separator, the error message contains the original string
@@ -4481,6 +4535,21 @@ def utcoffset(self, t):
44814535
t2 = t2.replace(tzinfo=Varies())
44824536
self.assertTrue(t1 < t2) # t1's offset counter still going up
44834537

4538+
def test_valuerror_messages(self):
4539+
pattern = re.compile(
4540+
r"(hour|minute|second|microsecond) must be in \d+\.\.\d+, not \d+"
4541+
)
4542+
test_cases = [
4543+
(12, 30, 90, 9999991), # Microsecond out of range
4544+
(12, 30, 90, 000000), # Second out of range
4545+
(25, 30, 45, 000000), # Hour out of range
4546+
(12, 90, 45, 000000), # Minute out of range
4547+
]
4548+
for case in test_cases:
4549+
with self.subTest(case):
4550+
with self.assertRaisesRegex(ValueError, pattern):
4551+
self.theclass(*case)
4552+
44844553
def test_fromisoformat(self):
44854554
time_examples = [
44864555
(0, 0, 0, 0),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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.

Diff for: Modules/_datetimemodule.c

+25-26
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,19 @@ check_date_args(int year, int month, int day)
637637
{
638638

639639
if (year < MINYEAR || year > MAXYEAR) {
640-
PyErr_Format(PyExc_ValueError, "year %i is out of range", year);
640+
PyErr_Format(PyExc_ValueError,
641+
"year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
641642
return -1;
642643
}
643644
if (month < 1 || month > 12) {
644-
PyErr_SetString(PyExc_ValueError,
645-
"month must be in 1..12");
645+
PyErr_Format(PyExc_ValueError,
646+
"month must be in 1..12, not %d", month);
646647
return -1;
647648
}
648-
if (day < 1 || day > days_in_month(year, month)) {
649-
PyErr_SetString(PyExc_ValueError,
650-
"day is out of range for month");
649+
int dim = days_in_month(year, month);
650+
if (day < 1 || day > dim) {
651+
PyErr_Format(PyExc_ValueError,
652+
"day must be in 1..%d, not %d", dim, day);
651653
return -1;
652654
}
653655
return 0;
@@ -660,28 +662,25 @@ static int
660662
check_time_args(int h, int m, int s, int us, int fold)
661663
{
662664
if (h < 0 || h > 23) {
663-
PyErr_SetString(PyExc_ValueError,
664-
"hour must be in 0..23");
665+
PyErr_Format(PyExc_ValueError, "hour must be in 0..23, not %i", h);
665666
return -1;
666667
}
667668
if (m < 0 || m > 59) {
668-
PyErr_SetString(PyExc_ValueError,
669-
"minute must be in 0..59");
669+
PyErr_Format(PyExc_ValueError, "minute must be in 0..59, not %i", m);
670670
return -1;
671671
}
672672
if (s < 0 || s > 59) {
673-
PyErr_SetString(PyExc_ValueError,
674-
"second must be in 0..59");
673+
PyErr_Format(PyExc_ValueError, "second must be in 0..59, not %i", s);
675674
return -1;
676675
}
677676
if (us < 0 || us > 999999) {
678-
PyErr_SetString(PyExc_ValueError,
679-
"microsecond must be in 0..999999");
677+
PyErr_Format(PyExc_ValueError,
678+
"microsecond must be in 0..999999, not %i", us);
680679
return -1;
681680
}
682681
if (fold != 0 && fold != 1) {
683-
PyErr_SetString(PyExc_ValueError,
684-
"fold must be either 0 or 1");
682+
PyErr_Format(PyExc_ValueError,
683+
"fold must be either 0 or 1, not %i", fold);
685684
return -1;
686685
}
687686
return 0;
@@ -1435,8 +1434,7 @@ new_timezone(PyObject *offset, PyObject *name)
14351434
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
14361435
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
14371436
" strictly between -timedelta(hours=24) and"
1438-
" timedelta(hours=24),"
1439-
" not %R.", offset);
1437+
" timedelta(hours=24), not %R", offset);
14401438
return NULL;
14411439
}
14421440

@@ -1505,10 +1503,10 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg)
15051503
GET_TD_SECONDS(offset) == 0 &&
15061504
GET_TD_MICROSECONDS(offset) < 1) ||
15071505
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
1508-
Py_DECREF(offset);
15091506
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
15101507
" strictly between -timedelta(hours=24) and"
1511-
" timedelta(hours=24).");
1508+
" timedelta(hours=24), not %R", offset);
1509+
Py_DECREF(offset);
15121510
return NULL;
15131511
}
15141512
}
@@ -2261,7 +2259,7 @@ get_float_as_integer_ratio(PyObject *floatobj)
22612259
if (!PyTuple_Check(ratio)) {
22622260
PyErr_Format(PyExc_TypeError,
22632261
"unexpected return type from as_integer_ratio(): "
2264-
"expected tuple, got '%.200s'",
2262+
"expected tuple, not '%.200s'",
22652263
Py_TYPE(ratio)->tp_name);
22662264
Py_DECREF(ratio);
22672265
return NULL;
@@ -3382,7 +3380,8 @@ date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw)
33823380
int rv = iso_to_ymd(year, week, day, &year, &month, &day);
33833381

33843382
if (rv == -4) {
3385-
PyErr_Format(PyExc_ValueError, "Year is out of range: %d", year);
3383+
PyErr_Format(PyExc_ValueError,
3384+
"year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
33863385
return NULL;
33873386
}
33883387

@@ -3392,7 +3391,7 @@ date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw)
33923391
}
33933392

33943393
if (rv == -3) {
3395-
PyErr_Format(PyExc_ValueError, "Invalid day: %d (range is [1, 7])",
3394+
PyErr_Format(PyExc_ValueError, "Invalid weekday: %d (range is [1, 7])",
33963395
day);
33973396
return NULL;
33983397
}
@@ -4378,8 +4377,7 @@ timezone_fromutc(PyDateTime_TimeZone *self, PyDateTime_DateTime *dt)
43784377
return NULL;
43794378
}
43804379
if (!HASTZINFO(dt) || dt->tzinfo != (PyObject *)self) {
4381-
PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo "
4382-
"is not self");
4380+
PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo is not self");
43834381
return NULL;
43844382
}
43854383

@@ -5352,7 +5350,8 @@ utc_to_seconds(int year, int month, int day,
53525350

53535351
/* ymd_to_ord() doesn't support year <= 0 */
53545352
if (year < MINYEAR || year > MAXYEAR) {
5355-
PyErr_Format(PyExc_ValueError, "year %i is out of range", year);
5353+
PyErr_Format(PyExc_ValueError,
5354+
"year must be in %d..%d, not %d", MINYEAR, MAXYEAR, year);
53565355
return -1;
53575356
}
53585357

0 commit comments

Comments
 (0)