Skip to content

Commit f4620f8

Browse files
jbrockmendelKevin D Smith
authored and
Kevin D Smith
committed
REF: de-duplicate DTA/TDA validators by standardizing exception messages (pandas-dev#37293)
1 parent aa0c40e commit f4620f8

File tree

7 files changed

+54
-33
lines changed

7 files changed

+54
-33
lines changed

pandas/core/arrays/datetimelike.py

+41-22
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def _from_factorized(cls, values, original):
419419
# Validation Methods
420420
# TODO: try to de-duplicate these, ensure identical behavior
421421

422-
def _validate_comparison_value(self, other, opname: str):
422+
def _validate_comparison_value(self, other):
423423
if isinstance(other, str):
424424
try:
425425
# GH#18435 strings get a pass from tzawareness compat
@@ -429,7 +429,7 @@ def _validate_comparison_value(self, other, opname: str):
429429
raise InvalidComparison(other)
430430

431431
if isinstance(other, self._recognized_scalars) or other is NaT:
432-
other = self._scalar_type(other) # type: ignore[call-arg]
432+
other = self._scalar_type(other)
433433
try:
434434
self._check_compatible_with(other)
435435
except TypeError as err:
@@ -477,7 +477,7 @@ def _validate_fill_value(self, fill_value):
477477
f"Got '{str(fill_value)}'."
478478
)
479479
try:
480-
fill_value = self._validate_scalar(fill_value, msg)
480+
fill_value = self._validate_scalar(fill_value)
481481
except TypeError as err:
482482
raise ValueError(msg) from err
483483
return self._unbox(fill_value)
@@ -509,17 +509,16 @@ def _validate_shift_value(self, fill_value):
509509

510510
return self._unbox(fill_value)
511511

512-
def _validate_scalar(self, value, msg: Optional[str] = None):
512+
def _validate_scalar(self, value, allow_listlike: bool = False):
513513
"""
514514
Validate that the input value can be cast to our scalar_type.
515515
516516
Parameters
517517
----------
518518
value : object
519-
msg : str, optional.
520-
Message to raise in TypeError on invalid input.
521-
If not provided, `value` is cast to a str and used
522-
as the message.
519+
allow_listlike: bool, default False
520+
When raising an exception, whether the message should say
521+
listlike inputs are allowed.
523522
524523
Returns
525524
-------
@@ -530,6 +529,7 @@ def _validate_scalar(self, value, msg: Optional[str] = None):
530529
try:
531530
value = self._scalar_from_string(value)
532531
except ValueError as err:
532+
msg = self._validation_error_message(value, allow_listlike)
533533
raise TypeError(msg) from err
534534

535535
elif is_valid_nat_for_dtype(value, self.dtype):
@@ -541,12 +541,38 @@ def _validate_scalar(self, value, msg: Optional[str] = None):
541541
value = self._scalar_type(value) # type: ignore[call-arg]
542542

543543
else:
544-
if msg is None:
545-
msg = str(value)
544+
msg = self._validation_error_message(value, allow_listlike)
546545
raise TypeError(msg)
547546

548547
return value
549548

549+
def _validation_error_message(self, value, allow_listlike: bool = False) -> str:
550+
"""
551+
Construct an exception message on validation error.
552+
553+
Some methods allow only scalar inputs, while others allow either scalar
554+
or listlike.
555+
556+
Parameters
557+
----------
558+
allow_listlike: bool, default False
559+
560+
Returns
561+
-------
562+
str
563+
"""
564+
if allow_listlike:
565+
msg = (
566+
f"value should be a '{self._scalar_type.__name__}', 'NaT', "
567+
f"or array of those. Got '{type(value).__name__}' instead."
568+
)
569+
else:
570+
msg = (
571+
f"value should be a '{self._scalar_type.__name__}' or 'NaT'. "
572+
f"Got '{type(value).__name__}' instead."
573+
)
574+
return msg
575+
550576
def _validate_listlike(self, value, allow_object: bool = False):
551577
if isinstance(value, type(self)):
552578
return value
@@ -583,36 +609,29 @@ def _validate_listlike(self, value, allow_object: bool = False):
583609
return value
584610

585611
def _validate_searchsorted_value(self, value):
586-
msg = "searchsorted requires compatible dtype or scalar"
587612
if not is_list_like(value):
588-
value = self._validate_scalar(value, msg)
613+
value = self._validate_scalar(value, True)
589614
else:
590615
value = self._validate_listlike(value)
591616

592617
return self._unbox(value)
593618

594619
def _validate_setitem_value(self, value):
595-
msg = (
596-
f"'value' should be a '{self._scalar_type.__name__}', 'NaT', "
597-
f"or array of those. Got '{type(value).__name__}' instead."
598-
)
599620
if is_list_like(value):
600621
value = self._validate_listlike(value)
601622
else:
602-
value = self._validate_scalar(value, msg)
623+
value = self._validate_scalar(value, True)
603624

604625
return self._unbox(value, setitem=True)
605626

606627
def _validate_insert_value(self, value):
607-
msg = f"cannot insert {type(self).__name__} with incompatible label"
608-
value = self._validate_scalar(value, msg)
628+
value = self._validate_scalar(value)
609629

610630
return self._unbox(value, setitem=True)
611631

612632
def _validate_where_value(self, other):
613-
msg = f"Where requires matching dtype, not {type(other)}"
614633
if not is_list_like(other):
615-
other = self._validate_scalar(other, msg)
634+
other = self._validate_scalar(other, True)
616635
else:
617636
other = self._validate_listlike(other)
618637

@@ -844,7 +863,7 @@ def _cmp_method(self, other, op):
844863
return op(self.ravel(), other.ravel()).reshape(self.shape)
845864

846865
try:
847-
other = self._validate_comparison_value(other, f"__{op.__name__}__")
866+
other = self._validate_comparison_value(other)
848867
except InvalidComparison:
849868
return invalid_comparison(self, other, op)
850869

pandas/tests/arrays/test_datetimelike.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ def test_setitem_raises(self):
399399
with pytest.raises(IndexError, match="index 12 is out of bounds"):
400400
arr[12] = val
401401

402-
with pytest.raises(TypeError, match="'value' should be a.* 'object'"):
402+
with pytest.raises(TypeError, match="value should be a.* 'object'"):
403403
arr[0] = object()
404404

405405
msg = "cannot set using a list-like indexer with a different length"
@@ -1032,7 +1032,7 @@ def test_casting_nat_setitem_array(array, casting_nats):
10321032
)
10331033
def test_invalid_nat_setitem_array(array, non_casting_nats):
10341034
msg = (
1035-
"'value' should be a '(Timestamp|Timedelta|Period)', 'NaT', or array of those. "
1035+
"value should be a '(Timestamp|Timedelta|Period)', 'NaT', or array of those. "
10361036
"Got '(timedelta64|datetime64|int)' instead."
10371037
)
10381038

pandas/tests/indexes/datetimes/test_insert.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def test_insert_nat(self, tz, null):
2121
@pytest.mark.parametrize("tz", [None, "UTC", "US/Eastern"])
2222
def test_insert_invalid_na(self, tz):
2323
idx = DatetimeIndex(["2017-01-01"], tz=tz)
24-
with pytest.raises(TypeError, match="incompatible label"):
24+
msg = "value should be a 'Timestamp' or 'NaT'. Got 'timedelta64' instead."
25+
with pytest.raises(TypeError, match=msg):
2526
idx.insert(0, np.timedelta64("NaT"))
2627

2728
def test_insert_empty_preserves_freq(self, tz_naive_fixture):
@@ -174,7 +175,7 @@ def test_insert_mismatched_types_raises(self, tz_aware_fixture, item):
174175
tz = tz_aware_fixture
175176
dti = date_range("2019-11-04", periods=9, freq="-1D", name=9, tz=tz)
176177

177-
msg = "incompatible label"
178+
msg = "value should be a 'Timestamp' or 'NaT'. Got '.*' instead"
178179
with pytest.raises(TypeError, match=msg):
179180
dti.insert(1, item)
180181

pandas/tests/indexes/timedeltas/test_insert.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def test_insert_nat(self, null):
7979

8080
def test_insert_invalid_na(self):
8181
idx = TimedeltaIndex(["4day", "1day", "2day"], name="idx")
82-
with pytest.raises(TypeError, match="incompatible label"):
82+
msg = r"value should be a 'Timedelta' or 'NaT'\. Got 'datetime64' instead\."
83+
with pytest.raises(TypeError, match=msg):
8384
idx.insert(0, np.datetime64("NaT"))
8485

8586
@pytest.mark.parametrize(
@@ -89,7 +90,7 @@ def test_insert_mismatched_types_raises(self, item):
8990
# GH#33703 dont cast these to td64
9091
tdi = TimedeltaIndex(["4day", "1day", "2day"], name="idx")
9192

92-
msg = "incompatible label"
93+
msg = r"value should be a 'Timedelta' or 'NaT'\. Got '.*' instead\."
9394
with pytest.raises(TypeError, match=msg):
9495
tdi.insert(1, item)
9596

pandas/tests/indexing/test_coercion.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def test_insert_index_datetimes(self, fill_val, exp_dtype):
444444
with pytest.raises(TypeError, match=msg):
445445
obj.insert(1, pd.Timestamp("2012-01-01", tz="Asia/Tokyo"))
446446

447-
msg = "cannot insert DatetimeArray with incompatible label"
447+
msg = "value should be a 'Timestamp' or 'NaT'. Got 'int' instead."
448448
with pytest.raises(TypeError, match=msg):
449449
obj.insert(1, 1)
450450

@@ -461,12 +461,12 @@ def test_insert_index_timedelta64(self):
461461
)
462462

463463
# ToDo: must coerce to object
464-
msg = "cannot insert TimedeltaArray with incompatible label"
464+
msg = "value should be a 'Timedelta' or 'NaT'. Got 'Timestamp' instead."
465465
with pytest.raises(TypeError, match=msg):
466466
obj.insert(1, pd.Timestamp("2012-01-01"))
467467

468468
# ToDo: must coerce to object
469-
msg = "cannot insert TimedeltaArray with incompatible label"
469+
msg = "value should be a 'Timedelta' or 'NaT'. Got 'int' instead."
470470
with pytest.raises(TypeError, match=msg):
471471
obj.insert(1, 1)
472472

pandas/tests/indexing/test_partial.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ def test_partial_set_invalid(self):
335335
df = orig.copy()
336336

337337
# don't allow not string inserts
338-
msg = "cannot insert DatetimeArray with incompatible label"
338+
msg = r"value should be a 'Timestamp' or 'NaT'\. Got '.*' instead\."
339339

340340
with pytest.raises(TypeError, match=msg):
341341
df.loc[100.0, :] = df.iloc[0]

pandas/tests/internals/test_internals.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@ def test_datetime_block_can_hold_element(self):
10951095
assert not block._can_hold_element(val)
10961096

10971097
msg = (
1098-
"'value' should be a 'Timestamp', 'NaT', "
1098+
"value should be a 'Timestamp', 'NaT', "
10991099
"or array of those. Got 'date' instead."
11001100
)
11011101
with pytest.raises(TypeError, match=msg):

0 commit comments

Comments
 (0)