From ff4044be1a2002fa0783a93397bca8816ff05f20 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 7 Jan 2020 17:19:10 -0800 Subject: [PATCH 1/5] REF: use shareable code for DTI/TDI.insert --- pandas/core/indexes/datetimes.py | 19 +++++++++---------- pandas/core/indexes/timedeltas.py | 20 +++++++++++--------- pandas/tests/arithmetic/test_datetime64.py | 3 +-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 88b841e7d4a88..b88a420446e62 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -922,7 +922,9 @@ def insert(self, loc, item): ------- new_index : Index """ - if is_valid_nat_for_dtype(item, self.dtype): + if isinstance(item, self._data._recognized_scalars): + item = self._data._scalar_type(item) + elif is_valid_nat_for_dtype(item, self.dtype): # GH 18295 item = self._na_value elif is_scalar(item) and isna(item): @@ -932,11 +934,8 @@ def insert(self, loc, item): ) freq = None - - if isinstance(item, (datetime, np.datetime64)): - self._assert_can_do_op(item) - if not self._has_same_tz(item) and not isna(item): - raise ValueError("Passed item and index have different timezone") + if isinstance(item, self._data._scalar_type) or item is NaT: + self._data._check_compatible_with(item, setitem=True) # check freq can be preserved on edge cases if self.size and self.freq is not None: @@ -946,19 +945,19 @@ def insert(self, loc, item): freq = self.freq elif (loc == len(self)) and item - self.freq == self[-1]: freq = self.freq - item = _to_M8(item, tz=self.tz) + item = item.asm8 try: - new_dates = np.concatenate( + new_i8s = np.concatenate( (self[:loc].asi8, [item.view(np.int64)], self[loc:].asi8) ) - return self._shallow_copy(new_dates, freq=freq) + return self._shallow_copy(new_i8s, freq=freq) except (AttributeError, TypeError): # fall back to object index if isinstance(item, str): return self.astype(object).insert(loc, item) - raise TypeError("cannot insert DatetimeIndex with incompatible label") + raise TypeError(f"cannot insert {type(self).__name__} with incompatible label") def indexer_at_time(self, time, asof=False): """ diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 59fc53a17590c..0a27fb9c90e1e 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -398,7 +398,7 @@ def insert(self, loc, item): """ # try to convert if possible if isinstance(item, self._data._recognized_scalars): - item = Timedelta(item) + item = self._data._scalar_type(item) elif is_valid_nat_for_dtype(item, self.dtype): # GH 18295 item = self._na_value @@ -409,28 +409,30 @@ def insert(self, loc, item): ) freq = None - if isinstance(item, Timedelta) or (is_scalar(item) and isna(item)): + if isinstance(item, self._data._scalar_type) or item is NaT: + self._data._check_compatible_with(item, setitem=True) # check freq can be preserved on edge cases - if self.freq is not None: - if (loc == 0 or loc == -len(self)) and item + self.freq == self[0]: + if self.size and self.freq is not None: + if item is NaT: + pass + elif (loc == 0 or loc == -len(self)) and item + self.freq == self[0]: freq = self.freq elif (loc == len(self)) and item - self.freq == self[-1]: freq = self.freq - item = Timedelta(item).asm8.view(_TD_DTYPE) + item = item.asm8 try: - new_tds = np.concatenate( + new_i8s = np.concatenate( (self[:loc].asi8, [item.view(np.int64)], self[loc:].asi8) ) - return self._shallow_copy(new_tds, freq=freq) - + return self._shallow_copy(new_i8s, freq=freq) except (AttributeError, TypeError): # fall back to object index if isinstance(item, str): return self.astype(object).insert(loc, item) - raise TypeError("cannot insert TimedeltaIndex with incompatible label") + raise TypeError(f"cannot insert {type(self).__name__} with incompatible label") TimedeltaIndex._add_comparison_ops() diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 1dfd95551f68d..d3f9ac4f3f8b2 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -27,7 +27,6 @@ date_range, ) import pandas._testing as tm -from pandas.core.indexes.datetimes import _to_M8 from pandas.core.ops import roperator from pandas.tests.arithmetic.common import ( assert_invalid_addsub_type, @@ -341,7 +340,7 @@ class TestDatetimeIndexComparisons: def test_comparators(self, op): index = tm.makeDateIndex(100) element = index[len(index) // 2] - element = _to_M8(element) + element = Timestamp(element).to_datetime64() arr = np.array(index) arr_result = op(arr, element) From 24ea5d4b81b4587815b2bcc76c801a8a7eae1d6e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 7 Jan 2020 17:22:20 -0800 Subject: [PATCH 2/5] flake8 fixup --- pandas/core/indexes/datetimes.py | 4 +++- pandas/core/indexes/timedeltas.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index b88a420446e62..70c62cc9a88d0 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -957,7 +957,9 @@ def insert(self, loc, item): # fall back to object index if isinstance(item, str): return self.astype(object).insert(loc, item) - raise TypeError(f"cannot insert {type(self).__name__} with incompatible label") + raise TypeError( + f"cannot insert {type(self).__name__} with incompatible label" + ) def indexer_at_time(self, time, asof=False): """ diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 0a27fb9c90e1e..5a82c24555e31 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -432,7 +432,9 @@ def insert(self, loc, item): # fall back to object index if isinstance(item, str): return self.astype(object).insert(loc, item) - raise TypeError(f"cannot insert {type(self).__name__} with incompatible label") + raise TypeError( + f"cannot insert {type(self).__name__} with incompatible label" + ) TimedeltaIndex._add_comparison_ops() From a39e50b657da0ec6307f5d62a08712ffcd1439e0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 7 Jan 2020 17:49:10 -0800 Subject: [PATCH 3/5] update tests --- pandas/tests/indexes/datetimes/test_indexing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_indexing.py b/pandas/tests/indexes/datetimes/test_indexing.py index 97290c8c635b8..1153d1619899c 100644 --- a/pandas/tests/indexes/datetimes/test_indexing.py +++ b/pandas/tests/indexes/datetimes/test_indexing.py @@ -434,9 +434,9 @@ def test_insert(self): # see gh-7299 idx = date_range("1/1/2000", periods=3, freq="D", tz="Asia/Tokyo", name="idx") - with pytest.raises(ValueError): + with pytest.raises(TypeError, match="Cannot compare tz-naive and tz-aware"): idx.insert(3, pd.Timestamp("2000-01-04")) - with pytest.raises(ValueError): + with pytest.raises(TypeError, match="Cannot compare tz-naive and tz-aware"): idx.insert(3, datetime(2000, 1, 4)) with pytest.raises(ValueError): idx.insert(3, pd.Timestamp("2000-01-04", tz="US/Eastern")) From ed4e2e7cf375c61e7bc9afe2c52dc2f2ccea6eb4 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 7 Jan 2020 17:52:02 -0800 Subject: [PATCH 4/5] update tests --- pandas/tests/indexing/test_coercion.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index 8843f6c08fe80..b904755b099d0 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -432,13 +432,19 @@ def test_insert_index_datetimes(self, fill_val, exp_dtype): ) self._assert_insert_conversion(obj, fill_val, exp, exp_dtype) - msg = "Passed item and index have different timezone" if fill_val.tz: - with pytest.raises(ValueError, match=msg): + msg = "Cannot compare tz-naive and tz-aware" + with pytest.raises(TypeError, match=msg): obj.insert(1, pd.Timestamp("2012-01-01")) - with pytest.raises(ValueError, match=msg): - obj.insert(1, pd.Timestamp("2012-01-01", tz="Asia/Tokyo")) + msg = "Timezones don't match" + with pytest.raises(ValueError, match=msg): + obj.insert(1, pd.Timestamp("2012-01-01", tz="Asia/Tokyo")) + + else: + msg = "Cannot compare tz-naive and tz-aware" + with pytest.raises(TypeError, match=msg): + obj.insert(1, pd.Timestamp("2012-01-01", tz="Asia/Tokyo")) msg = "cannot insert DatetimeIndex with incompatible label" with pytest.raises(TypeError, match=msg): From 7ff4fb809a08a9b155ca181fffa82745640e9a1b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 8 Jan 2020 10:34:26 -0800 Subject: [PATCH 5/5] whatsnew --- doc/source/whatsnew/v1.0.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 78a8ba5cddea0..40737899ba98e 100755 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -872,6 +872,7 @@ Datetimelike - Bug in :func:`pandas.to_datetime` when called with ``Series`` storing ``IntegerArray`` raising ``TypeError`` instead of returning ``Series`` (:issue:`30050`) - Bug in :func:`date_range` with custom business hours as ``freq`` and given number of ``periods`` (:issue:`30593`) - Bug in :class:`PeriodIndex` comparisons with incorrectly casting integers to :class:`Period` objects, inconsistent with the :class:`Period` comparison behavior (:issue:`30722`) +- Bug in :meth:`DatetimeIndex.insert` raising a ``ValueError`` instead of a ``TypeError`` when trying to insert a timezone-aware :class:`Timestamp` into a timezone-naive :class:`DatetimeIndex`, or vice-versa (:issue:`30806`) Timedelta ^^^^^^^^^