From 9135c4b6a0f246ebf0a2fe7571b20f9f73f66bfb Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 12 Feb 2021 20:41:05 -0800 Subject: [PATCH 1/4] BUG: DTA/TDA/PA/Series/Index.view with datetimelike --- pandas/core/arrays/datetimelike.py | 22 +++++++++++++++++++-- pandas/core/indexes/base.py | 17 ++++++++++++++++ pandas/tests/series/methods/test_view.py | 25 +++++++++++++++++++++++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 162a69370bc61..248a9ea9cd78f 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -34,7 +34,7 @@ ) from pandas._libs.tslibs.fields import RoundTo, round_nsint64 from pandas._libs.tslibs.timestamps import integer_op_not_supported -from pandas._typing import DatetimeLikeScalar, Dtype, DtypeObj, NpDtype +from pandas._typing import ArrayLike, DatetimeLikeScalar, Dtype, DtypeObj, NpDtype from pandas.compat.numpy import function as nv from pandas.errors import AbstractMethodError, NullFrequencyError, PerformanceWarning from pandas.util._decorators import Appender, Substitution, cache_readonly @@ -57,6 +57,7 @@ is_unsigned_integer_dtype, pandas_dtype, ) +from pandas.core.dtypes.dtypes import DatetimeTZDtype, PeriodDtype from pandas.core.dtypes.missing import is_valid_na_for_dtype, isna from pandas.core import nanops, ops @@ -381,9 +382,26 @@ def astype(self, dtype, copy=True): else: return np.asarray(self, dtype=dtype) - def view(self, dtype: Optional[Dtype] = None): + def view(self, dtype: Optional[Dtype] = None) -> ArrayLike: if dtype is None or dtype is self.dtype: return type(self)(self._ndarray, dtype=self.dtype) + + if isinstance(dtype, type): + # e.g. np.ndarray + return self._ndarray.view(dtype) + + dtype = pandas_dtype(dtype) + if isinstance(dtype, (PeriodDtype, DatetimeTZDtype)): + cls = dtype.construct_array_type() + return cls._simple_new(self.asi8, dtype=dtype) + elif dtype == "M8[ns]": + from pandas.core.arrays import DatetimeArray + + return DatetimeArray._simple_new(self.asi8, dtype=dtype) + elif dtype == "m8[ns]": + from pandas.core.arrays import TimedeltaArray + + return TimedeltaArray._simple_new(self.asi8.view("m8[ns]"), dtype=dtype) return self._ndarray.view(dtype=dtype) # ------------------------------------------------------------------ diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 789ca04b894cd..305ef9eadb4ca 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -747,6 +747,23 @@ def view(self, cls=None): # we need to see if we are subclassing an # index type here if cls is not None and not hasattr(cls, "_typ"): + dtype = cls + if isinstance(cls, str): + dtype = pandas_dtype(cls) + + if isinstance(dtype, (np.dtype, ExtensionDtype)) and needs_i8_conversion( + dtype + ): + if dtype.kind == "m" and dtype != "m8[ns]": + # e.g. m8[s] + return self._data.view(cls) + + arr = self._data.view("i8") + idx_cls = self._dtype_to_subclass(dtype) + arr_cls = idx_cls._data_cls + arr = arr_cls._simple_new(self._data.view("i8"), dtype=dtype) + return idx_cls._simple_new(arr, name=self.name) + result = self._data.view(cls) else: result = self._view() diff --git a/pandas/tests/series/methods/test_view.py b/pandas/tests/series/methods/test_view.py index ccf3aa0d90e6f..9eaf62068d42b 100644 --- a/pandas/tests/series/methods/test_view.py +++ b/pandas/tests/series/methods/test_view.py @@ -1,4 +1,7 @@ -from pandas import Series, date_range +import numpy as np +import pytest + +from pandas import Index, Series, array as pd_array, date_range import pandas._testing as tm @@ -16,3 +19,23 @@ def test_view_tz(self): ] ) tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "first", ["m8[ns]", "M8[ns]", "M8[ns, US/Central]", "period[D]"] + ) + @pytest.mark.parametrize( + "second", ["m8[ns]", "M8[ns]", "M8[ns, US/Central]", "period[D]"] + ) + @pytest.mark.parametrize("box", [Series, Index, pd_array]) + def test_view_between_datetimelike(self, first, second, box): + + dti = date_range("2016-01-01", periods=3) + + orig = box(dti) + obj = orig.view(first) + assert obj.dtype == first + tm.assert_numpy_array_equal(np.asarray(obj.view("i8")), dti.asi8) + + res = obj.view(second) + assert res.dtype == second + tm.assert_numpy_array_equal(np.asarray(obj.view("i8")), dti.asi8) From e544be2c3b75cc26666187525d66354890786e63 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 12 Feb 2021 20:42:48 -0800 Subject: [PATCH 2/4] whatsnew --- doc/source/whatsnew/v1.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 99ae60859b68c..f3033e2e3aba6 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -312,7 +312,7 @@ Numeric Conversion ^^^^^^^^^^ -- +- Bug in :meth:`Series.view` and :meth:`Index.view` when converting between datetime-like (``datetime64[ns]``, ``datetime64[ns, tz]``, ``timedelta64``, ``period``) dtypes (:issue:`??`) - Strings From c216c7b58e06e4988a714ca3c755a882304bae7e Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 12 Feb 2021 20:44:19 -0800 Subject: [PATCH 3/4] GH ref --- doc/source/whatsnew/v1.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index f3033e2e3aba6..33e37087f3de8 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -312,7 +312,7 @@ Numeric Conversion ^^^^^^^^^^ -- Bug in :meth:`Series.view` and :meth:`Index.view` when converting between datetime-like (``datetime64[ns]``, ``datetime64[ns, tz]``, ``timedelta64``, ``period``) dtypes (:issue:`??`) +- Bug in :meth:`Series.view` and :meth:`Index.view` when converting between datetime-like (``datetime64[ns]``, ``datetime64[ns, tz]``, ``timedelta64``, ``period``) dtypes (:issue:`39788`) - Strings From c643e9cedc0c70fc6bac22bad631e1f7ddf2835e Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 17 Feb 2021 10:40:34 -0800 Subject: [PATCH 4/4] flesh out commens --- pandas/core/arrays/datetimelike.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 4a4978c6b2acb..a6ee95e131559 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -434,11 +434,15 @@ def astype(self, dtype, copy=True): return np.asarray(self, dtype=dtype) def view(self, dtype: Optional[Dtype] = None) -> ArrayLike: + # We handle datetime64, datetime64tz, timedelta64, and period + # dtypes here. Everything else we pass through to the underlying + # ndarray. if dtype is None or dtype is self.dtype: return type(self)(self._ndarray, dtype=self.dtype) if isinstance(dtype, type): - # e.g. np.ndarray + # we sometimes pass non-dtype objects, e.g np.ndarray; + # pass those through to the underlying ndarray return self._ndarray.view(dtype) dtype = pandas_dtype(dtype)