From 25a471afbc200e12a0388e7341cf846438e02d56 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 30 Sep 2022 14:01:46 -0700 Subject: [PATCH 1/7] BUG: Timedelta.__new__ --- doc/source/whatsnew/v1.6.0.rst | 2 ++ pandas/_libs/tslibs/timedeltas.pyx | 14 +++++++++++--- pandas/tests/scalar/timedelta/test_constructors.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.6.0.rst b/doc/source/whatsnew/v1.6.0.rst index 098750aa3a2b2..f9803d9ce0727 100644 --- a/doc/source/whatsnew/v1.6.0.rst +++ b/doc/source/whatsnew/v1.6.0.rst @@ -172,6 +172,8 @@ Datetimelike Timedelta ^^^^^^^^^ - Bug in :func:`to_timedelta` raising error when input has nullable dtype ``Float64`` (:issue:`48796`) +- Bug in :class:`Timedelta` constructor incorrectly raising instead of returning ``NaT`` when given a ``np.timedelta64("nat")`` (:issue:`?`) +- Bug in :class:`Timedelta` constructor failing to raise when passed both a :class:`Timedelta` object and keywords (e.g. days, seconds) (:issue:`?`) - Timezones diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index bf22967f615c4..99d768ce1f3d4 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1662,10 +1662,15 @@ class Timedelta(_Timedelta): # GH 30543 if pd.Timedelta already passed, return it # check that only value is passed - if isinstance(value, _Timedelta) and unit is None and len(kwargs) == 0: + if isinstance(value, _Timedelta): + # 'unit' is benign in this case, but e.g. days or seconds + # doesn't make sense here. + if len(kwargs): + assert False + raise ValueError( + "Cannot pass both a Timedelta input and timedelta keyword arguments." + ) return value - elif isinstance(value, _Timedelta): - value = value.value elif isinstance(value, str): if unit is not None: raise ValueError("unit must not be specified if the value is a str") @@ -1679,6 +1684,9 @@ class Timedelta(_Timedelta): elif PyDelta_Check(value): value = convert_to_timedelta64(value, 'ns') elif is_timedelta64_object(value): + if get_timedelta64_value(value) == NPY_NAT: + # i.e. np.timedelta64("NaT") + return NaT value = ensure_td64ns(value) elif is_tick_object(value): value = np.timedelta64(value.nanos, 'ns') diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 5b2438ec30f3a..c76d6f3279d06 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -7,6 +7,7 @@ from pandas._libs.tslibs import OutOfBoundsTimedelta from pandas import ( + NaT, Timedelta, offsets, to_timedelta, @@ -371,6 +372,14 @@ def test_timedelta_constructor_identity(): assert result is expected +def test_timedelta_pass_td_and_kwargs_raises(): + # don't silently ignore the kwargs + td = Timedelta(days=1) + msg = "Cannot pass both a Timedelta input and timedelta keyword arguments" + with pytest.raises(ValueError, match=msg): + Timedelta(td, days=2) + + @pytest.mark.parametrize( "constructor, value, unit, expectation", [ @@ -402,3 +411,8 @@ def test_string_without_numbers(value): ) with pytest.raises(ValueError, match=msg): Timedelta(value) + + +def test_timedelta_new_npnat(): + nat = np.timedelta64("NaT", "h") + assert Timedelta(nat) is NaT From 42e9553d1b9bcc0d25d123066544a8b84cfc0ca1 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 30 Sep 2022 14:02:53 -0700 Subject: [PATCH 2/7] remove assertion --- pandas/_libs/tslibs/timedeltas.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 99d768ce1f3d4..8d6c940db19ce 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1666,7 +1666,6 @@ class Timedelta(_Timedelta): # 'unit' is benign in this case, but e.g. days or seconds # doesn't make sense here. if len(kwargs): - assert False raise ValueError( "Cannot pass both a Timedelta input and timedelta keyword arguments." ) From e6b932371125ac7ba1a97911c685ef9f3dab6537 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 30 Sep 2022 14:04:23 -0700 Subject: [PATCH 3/7] GH refs --- doc/source/whatsnew/v1.6.0.rst | 4 ++-- pandas/_libs/tslibs/timedeltas.pyx | 1 + pandas/tests/scalar/timedelta/test_constructors.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.6.0.rst b/doc/source/whatsnew/v1.6.0.rst index f9803d9ce0727..c432f46ce4a14 100644 --- a/doc/source/whatsnew/v1.6.0.rst +++ b/doc/source/whatsnew/v1.6.0.rst @@ -172,8 +172,8 @@ Datetimelike Timedelta ^^^^^^^^^ - Bug in :func:`to_timedelta` raising error when input has nullable dtype ``Float64`` (:issue:`48796`) -- Bug in :class:`Timedelta` constructor incorrectly raising instead of returning ``NaT`` when given a ``np.timedelta64("nat")`` (:issue:`?`) -- Bug in :class:`Timedelta` constructor failing to raise when passed both a :class:`Timedelta` object and keywords (e.g. days, seconds) (:issue:`?`) +- Bug in :class:`Timedelta` constructor incorrectly raising instead of returning ``NaT`` when given a ``np.timedelta64("nat")`` (:issue:`48898`) +- Bug in :class:`Timedelta` constructor failing to raise when passed both a :class:`Timedelta` object and keywords (e.g. days, seconds) (:issue:`48898`) - Timezones diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 8d6c940db19ce..a434a52356e54 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1666,6 +1666,7 @@ class Timedelta(_Timedelta): # 'unit' is benign in this case, but e.g. days or seconds # doesn't make sense here. if len(kwargs): + # GH#48898 raise ValueError( "Cannot pass both a Timedelta input and timedelta keyword arguments." ) diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index c76d6f3279d06..25f75390b9d6c 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -373,7 +373,7 @@ def test_timedelta_constructor_identity(): def test_timedelta_pass_td_and_kwargs_raises(): - # don't silently ignore the kwargs + # don't silently ignore the kwargs GH#48898 td = Timedelta(days=1) msg = "Cannot pass both a Timedelta input and timedelta keyword arguments" with pytest.raises(ValueError, match=msg): @@ -414,5 +414,6 @@ def test_string_without_numbers(value): def test_timedelta_new_npnat(): + # GH#48898 nat = np.timedelta64("NaT", "h") assert Timedelta(nat) is NaT From 5b3ee02c7d540f6d7ab2c69ffddc58a94e45ef8e Mon Sep 17 00:00:00 2001 From: Brock Date: Sat, 1 Oct 2022 16:37:25 -0700 Subject: [PATCH 4/7] API: Timedelta(td64_obj) retain resolution --- pandas/_libs/tslibs/dtypes.pxd | 1 + pandas/_libs/tslibs/dtypes.pyi | 1 + pandas/_libs/tslibs/dtypes.pyx | 13 ++ pandas/_libs/tslibs/timedeltas.pxd | 1 + pandas/_libs/tslibs/timedeltas.pyx | 145 ++++++++---------- pandas/core/arrays/numpy_.py | 10 +- pandas/core/arrays/timedeltas.py | 4 + pandas/core/construction.py | 8 + pandas/core/window/ewm.py | 3 +- pandas/tests/arithmetic/test_numeric.py | 13 ++ pandas/tests/dtypes/cast/test_promote.py | 11 +- pandas/tests/frame/test_constructors.py | 59 +++++-- .../scalar/timedelta/test_constructors.py | 82 ++++++++-- .../tests/scalar/timedelta/test_timedelta.py | 15 +- 14 files changed, 246 insertions(+), 120 deletions(-) diff --git a/pandas/_libs/tslibs/dtypes.pxd b/pandas/_libs/tslibs/dtypes.pxd index 352680143113d..11b92447f5011 100644 --- a/pandas/_libs/tslibs/dtypes.pxd +++ b/pandas/_libs/tslibs/dtypes.pxd @@ -8,6 +8,7 @@ cdef NPY_DATETIMEUNIT abbrev_to_npy_unit(str abbrev) cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) nogil cpdef int64_t periods_per_day(NPY_DATETIMEUNIT reso=*) except? -1 cpdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1 +cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso) cdef dict attrname_to_abbrevs diff --git a/pandas/_libs/tslibs/dtypes.pyi b/pandas/_libs/tslibs/dtypes.pyi index 82f62e16c4205..a54db51136d07 100644 --- a/pandas/_libs/tslibs/dtypes.pyi +++ b/pandas/_libs/tslibs/dtypes.pyi @@ -9,6 +9,7 @@ def periods_per_day(reso: int) -> int: ... def periods_per_second(reso: int) -> int: ... def is_supported_unit(reso: int) -> bool: ... def npy_unit_to_abbrev(reso: int) -> str: ... +def get_supported_reso(reso: int) -> int: ... class PeriodDtypeBase: _dtype_code: int # PeriodDtypeCode diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index 2f847640d606e..94781374296fa 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -278,6 +278,19 @@ class NpyDatetimeUnit(Enum): NPY_FR_GENERIC = NPY_DATETIMEUNIT.NPY_FR_GENERIC +cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso): + # If we have an unsupported reso, return the nearest supported reso. + if reso == NPY_DATETIMEUNIT.NPY_FR_GENERIC: + # TODO: or raise ValueError? trying this gives unraisable errors, but + # "except? -1" breaks at compile-time for unknown reasons + return NPY_DATETIMEUNIT.NPY_FR_ns + if reso < NPY_DATETIMEUNIT.NPY_FR_s: + return NPY_DATETIMEUNIT.NPY_FR_s + elif reso > NPY_DATETIMEUNIT.NPY_FR_ns: + return NPY_DATETIMEUNIT.NPY_FR_ns + return reso + + def is_supported_unit(NPY_DATETIMEUNIT reso): return ( reso == NPY_DATETIMEUNIT.NPY_FR_ns diff --git a/pandas/_libs/tslibs/timedeltas.pxd b/pandas/_libs/tslibs/timedeltas.pxd index 3251e10a88b73..7c597cb4b102b 100644 --- a/pandas/_libs/tslibs/timedeltas.pxd +++ b/pandas/_libs/tslibs/timedeltas.pxd @@ -25,3 +25,4 @@ cdef class _Timedelta(timedelta): cdef _ensure_components(_Timedelta self) cdef inline bint _compare_mismatched_resos(self, _Timedelta other, op) cdef _Timedelta _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=*) + cpdef _maybe_cast_to_matching_resos(self, _Timedelta other) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index a434a52356e54..ee04750f337f1 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -38,7 +38,10 @@ from pandas._libs.tslibs.conversion cimport ( cast_from_unit, precision_from_unit, ) -from pandas._libs.tslibs.dtypes cimport npy_unit_to_abbrev +from pandas._libs.tslibs.dtypes cimport ( + get_supported_reso, + npy_unit_to_abbrev, +) from pandas._libs.tslibs.nattype cimport ( NPY_NAT, c_NaT as NaT, @@ -939,6 +942,7 @@ cdef _timedelta_from_value_and_reso(int64_t value, NPY_DATETIMEUNIT reso): cdef: _Timedelta td_base + assert value != NPY_NAT # For millisecond and second resos, we cannot actually pass int(value) because # many cases would fall outside of the pytimedelta implementation bounds. # We pass 0 instead, and override seconds, microseconds, days. @@ -1530,12 +1534,7 @@ cdef class _Timedelta(timedelta): def _as_unit(self, str unit, bint round_ok=True): dtype = np.dtype(f"m8[{unit}]") reso = get_unit_from_dtype(dtype) - try: - return self._as_reso(reso, round_ok=round_ok) - except OverflowError as err: - raise OutOfBoundsTimedelta( - f"Cannot cast {self} to unit='{unit}' without overflow." - ) from err + return self._as_reso(reso, round_ok=round_ok) @cython.cdivision(False) cdef _Timedelta _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=True): @@ -1545,9 +1544,26 @@ cdef class _Timedelta(timedelta): if reso == self._reso: return self - value = convert_reso(self.value, self._reso, reso, round_ok=round_ok) + try: + value = convert_reso(self.value, self._reso, reso, round_ok=round_ok) + except OverflowError as err: + unit = npy_unit_to_abbrev(reso) + raise OutOfBoundsTimedelta( + f"Cannot cast {self} to unit='{unit}' without overflow." + ) from err + return type(self)._from_value_and_reso(value, reso=reso) + cpdef _maybe_cast_to_matching_resos(self, _Timedelta other): + """ + If _resos do not match, cast to the higher resolution, raising on overflow. + """ + if self._reso > other._reso: + other = other._as_reso(self._reso) + elif self._reso < other._reso: + self = self._as_reso(other._reso) + return self, other + # Python front end to C extension type _Timedelta # This serves as the box for timedelta64 @@ -1684,10 +1700,27 @@ class Timedelta(_Timedelta): elif PyDelta_Check(value): value = convert_to_timedelta64(value, 'ns') elif is_timedelta64_object(value): - if get_timedelta64_value(value) == NPY_NAT: + # Retain the resolution if possible, otherwise cast to the nearest + # supported resolution. + new_value = get_timedelta64_value(value) + if new_value == NPY_NAT: # i.e. np.timedelta64("NaT") return NaT - value = ensure_td64ns(value) + + reso = get_datetime64_unit(value) + new_reso = get_supported_reso(reso) + if reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC: + try: + new_value = convert_reso( + get_timedelta64_value(value), + reso, + new_reso, + round_ok=True, + ) + except (OverflowError, OutOfBoundsDatetime) as err: + raise OutOfBoundsTimedelta(value) from err + return cls._from_value_and_reso(new_value, reso=new_reso) + elif is_tick_object(value): value = np.timedelta64(value.nanos, 'ns') elif is_integer_object(value) or is_float_object(value): @@ -1826,11 +1859,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if other._reso != self._reso: - raise ValueError( - "division between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return self.value / float(other.value) elif is_integer_object(other) or is_float_object(other): @@ -1857,11 +1886,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "division between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return float(other.value) / self.value elif is_array(other): @@ -1883,17 +1908,14 @@ class Timedelta(_Timedelta): def __floordiv__(self, other): # numpy does not implement floordiv for timedelta64 dtype, so we cannot # just defer + orig = other if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "floordivision between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return self.value // other.value elif is_integer_object(other) or is_float_object(other): @@ -1909,9 +1931,16 @@ class Timedelta(_Timedelta): if other.dtype.kind == 'm': # also timedelta-like - if self._reso != NPY_FR_ns: - raise NotImplementedError - return _broadcast_floordiv_td64(self.value, other, _floordiv) + # TODO: could suppress + # RuntimeWarning: invalid value encountered in floor_divide + result = self.asm8 // other + mask = other.view("i8") == NPY_NAT + if mask.any(): + # We differ from numpy here + result = result.astype("f8") + result[mask] = np.nan + return result + elif other.dtype.kind in ['i', 'u', 'f']: if other.ndim == 0: return self // other.item() @@ -1931,11 +1960,7 @@ class Timedelta(_Timedelta): if other is NaT: return np.nan if self._reso != other._reso: - raise ValueError( - "floordivision between Timedeltas with mismatched resolutions " - "are not supported. Explicitly cast to matching resolutions " - "before dividing." - ) + self, other = self._maybe_cast_to_matching_resos(other) return other.value // self.value elif is_array(other): @@ -1946,9 +1971,15 @@ class Timedelta(_Timedelta): if other.dtype.kind == 'm': # also timedelta-like - if self._reso != NPY_FR_ns: - raise NotImplementedError - return _broadcast_floordiv_td64(self.value, other, _rfloordiv) + # TODO: could suppress + # RuntimeWarning: invalid value encountered in floor_divide + result = other // self.asm8 + mask = other.view("i8") == NPY_NAT + if mask.any(): + # We differ from numpy here + result = result.astype("f8") + result[mask] = np.nan + return result # Includes integer array // Timedelta, disallowed in GH#19761 raise TypeError(f'Invalid dtype {other.dtype} for __floordiv__') @@ -1998,45 +2029,3 @@ cdef bint _should_cast_to_timedelta(object obj): return ( is_any_td_scalar(obj) or obj is None or obj is NaT or isinstance(obj, str) ) - - -cdef _floordiv(int64_t value, right): - return value // right - - -cdef _rfloordiv(int64_t value, right): - # analogous to referencing operator.div, but there is no operator.rfloordiv - return right // value - - -cdef _broadcast_floordiv_td64( - int64_t value, - ndarray other, - object (*operation)(int64_t value, object right) -): - """ - Boilerplate code shared by Timedelta.__floordiv__ and - Timedelta.__rfloordiv__ because np.timedelta64 does not implement these. - - Parameters - ---------- - value : int64_t; `self.value` from a Timedelta object - other : object - operation : function, either _floordiv or _rfloordiv - - Returns - ------- - result : varies based on `other` - """ - # assumes other.dtype.kind == 'm', i.e. other is timedelta-like - # assumes other.ndim != 0 - - # We need to watch out for np.timedelta64('NaT'). - mask = other.view('i8') == NPY_NAT - - res = operation(value, other.astype('m8[ns]', copy=False).astype('i8')) - - if mask.any(): - res = res.astype('f8') - res[mask] = np.nan - return res diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index 4d5286e7364f5..48679a8355837 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -3,6 +3,10 @@ import numpy as np from pandas._libs import lib +from pandas._libs.tslibs import ( + get_unit_from_dtype, + is_supported_unit, +) from pandas._typing import ( AxisInt, Dtype, @@ -439,10 +443,12 @@ def _cmp_method(self, other, op): def _wrap_ndarray_result(self, result: np.ndarray): # If we have timedelta64[ns] result, return a TimedeltaArray instead # of a PandasArray - if result.dtype == "timedelta64[ns]": + if result.dtype.kind == "m" and is_supported_unit( + get_unit_from_dtype(result.dtype) + ): from pandas.core.arrays import TimedeltaArray - return TimedeltaArray._simple_new(result) + return TimedeltaArray._simple_new(result, dtype=result.dtype) return type(self)(result) # ------------------------------------------------------------------------ diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 4c3e790c2879b..2c97ce2fce242 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -284,6 +284,10 @@ def _unbox_scalar(self, value, setitem: bool = False) -> np.timedelta64: if not isinstance(value, self._scalar_type) and value is not NaT: raise ValueError("'value' should be a Timedelta.") self._check_compatible_with(value, setitem=setitem) + if value is NaT: + return np.timedelta64(value.value, "ns") + else: + return value._as_unit(self._unit).asm8 return np.timedelta64(value.value, "ns") def _scalar_from_string(self, value) -> Timedelta | NaTType: diff --git a/pandas/core/construction.py b/pandas/core/construction.py index e1a69086609e9..e9b41ce43230d 100644 --- a/pandas/core/construction.py +++ b/pandas/core/construction.py @@ -49,6 +49,7 @@ ) from pandas.core.dtypes.common import ( is_datetime64_ns_dtype, + is_dtype_equal, is_extension_array_dtype, is_float_dtype, is_integer_dtype, @@ -324,6 +325,13 @@ def array( data = extract_array(data, extract_numpy=True) + if isinstance(data, ExtensionArray) and ( + dtype is None or is_dtype_equal(dtype, data.dtype) + ): + if copy: + return data.copy() + return data + # this returns None for not-found dtypes. if isinstance(dtype, str): dtype = registry.find(dtype) or dtype diff --git a/pandas/core/window/ewm.py b/pandas/core/window/ewm.py index 297febe724019..704c285197456 100644 --- a/pandas/core/window/ewm.py +++ b/pandas/core/window/ewm.py @@ -134,7 +134,8 @@ def _calculate_deltas( _times = np.asarray( times.view(np.int64), dtype=np.float64 # type: ignore[union-attr] ) - _halflife = float(Timedelta(halflife).value) + # TODO: generalize to non-nano? + _halflife = float(Timedelta(halflife)._as_unit("ns").value) return np.diff(_times) / _halflife diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 881a5f1de1c60..f63e70ced4b52 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -204,6 +204,11 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box_with_array): box = box_with_array index = numeric_idx expected = TimedeltaIndex([Timedelta(days=n) for n in range(len(index))]) + if isinstance(scalar_td, np.timedelta64) and box not in [Index, Series]: + # TODO(2.0): once TDA.astype converts to m8, just do expected.astype + tda = expected._data + dtype = scalar_td.dtype + expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) @@ -249,6 +254,14 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box_with_array index = numeric_idx[1:3] expected = TimedeltaIndex(["3 Days", "36 Hours"]) + if isinstance(three_days, np.timedelta64) and box not in [Index, Series]: + # TODO(2.0): just use expected.astype + tda = expected._data + dtype = three_days.dtype + if dtype < np.dtype("m8[s]"): + # i.e. resolution is lower -> use lowest supported resolution + dtype = np.dtype("m8[s]") + expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) diff --git a/pandas/tests/dtypes/cast/test_promote.py b/pandas/tests/dtypes/cast/test_promote.py index 02bd03f5ea266..c07e0a187babe 100644 --- a/pandas/tests/dtypes/cast/test_promote.py +++ b/pandas/tests/dtypes/cast/test_promote.py @@ -463,7 +463,9 @@ def test_maybe_promote_timedelta64_with_any(timedelta64_dtype, any_numpy_dtype_r [pd.Timedelta(days=1), np.timedelta64(24, "h"), datetime.timedelta(1)], ids=["pd.Timedelta", "np.timedelta64", "datetime.timedelta"], ) -def test_maybe_promote_any_with_timedelta64(any_numpy_dtype_reduced, fill_value): +def test_maybe_promote_any_with_timedelta64( + any_numpy_dtype_reduced, fill_value, request +): dtype = np.dtype(any_numpy_dtype_reduced) # filling anything but timedelta with timedelta casts to object @@ -471,6 +473,13 @@ def test_maybe_promote_any_with_timedelta64(any_numpy_dtype_reduced, fill_value) expected_dtype = dtype # for timedelta dtypes, scalar values get cast to pd.Timedelta.value exp_val_for_scalar = pd.Timedelta(fill_value).to_timedelta64() + + if isinstance(fill_value, np.timedelta64) and fill_value.dtype != "m8[ns]": + mark = pytest.mark.xfail( + reason="maybe_promote not yet updated to handle non-nano " + "Timedelta scalar" + ) + request.node.add_marker(mark) else: expected_dtype = np.dtype(object) exp_val_for_scalar = fill_value diff --git a/pandas/tests/frame/test_constructors.py b/pandas/tests/frame/test_constructors.py index 37e08adcfdf88..02313e429f3b6 100644 --- a/pandas/tests/frame/test_constructors.py +++ b/pandas/tests/frame/test_constructors.py @@ -856,16 +856,31 @@ def create_data(constructor): tm.assert_frame_equal(result_datetime, expected) tm.assert_frame_equal(result_Timestamp, expected) - def test_constructor_dict_timedelta64_index(self): + @pytest.mark.parametrize( + "klass", + [ + pytest.param( + np.timedelta64, + marks=pytest.mark.xfail( + reason="hash mismatch (GH#44504) causes lib.fast_multiget " + "to mess up on dict lookups with equal Timedeltas with " + "mismatched resos" + ), + ), + timedelta, + Timedelta, + ], + ) + def test_constructor_dict_timedelta64_index(self, klass): # GH 10160 td_as_int = [1, 2, 3, 4] - def create_data(constructor): - return {i: {constructor(s): 2 * i} for i, s in enumerate(td_as_int)} + if klass is timedelta: + constructor = lambda x: timedelta(days=x) + else: + constructor = lambda x: klass(x, "D") - data_timedelta64 = create_data(lambda x: np.timedelta64(x, "D")) - data_timedelta = create_data(lambda x: timedelta(days=x)) - data_Timedelta = create_data(lambda x: Timedelta(x, "D")) + data = {i: {constructor(s): 2 * i} for i, s in enumerate(td_as_int)} expected = DataFrame( [ @@ -877,12 +892,8 @@ def create_data(constructor): index=[Timedelta(td, "D") for td in td_as_int], ) - result_timedelta64 = DataFrame(data_timedelta64) - result_timedelta = DataFrame(data_timedelta) - result_Timedelta = DataFrame(data_Timedelta) - tm.assert_frame_equal(result_timedelta64, expected) - tm.assert_frame_equal(result_timedelta, expected) - tm.assert_frame_equal(result_Timedelta, expected) + result = DataFrame(data) + tm.assert_frame_equal(result, expected) def test_constructor_period_dict(self): # PeriodIndex @@ -3111,14 +3122,34 @@ def test_from_out_of_bounds_datetime(self, constructor, cls): assert type(get1(result)) is cls + @pytest.mark.xfail( + reason="TimedeltaArray constructor has been updated to cast td64 to non-nano, " + "but TimedeltaArray._from_sequence has not" + ) @pytest.mark.parametrize("cls", [timedelta, np.timedelta64]) - def test_from_out_of_bounds_timedelta(self, constructor, cls): + def test_from_out_of_bounds_ns_timedelta(self, constructor, cls): + # scalar that won't fit in nanosecond td64, but will fit in microsecond scalar = datetime(9999, 1, 1) - datetime(1970, 1, 1) + exp_dtype = "m8[us]" # smallest reso that fits if cls is np.timedelta64: scalar = np.timedelta64(scalar, "D") + exp_dtype = "m8[s]" # closest reso to input result = constructor(scalar) - assert type(get1(result)) is cls + item = get1(result) + dtype = result.dtype if isinstance(result, Series) else result.dtypes.iloc[0] + + assert type(item) is Timedelta + assert item.asm8.dtype == exp_dtype + assert dtype == exp_dtype + + def test_out_of_s_bounds_timedelta64(self, constructor): + scalar = np.timedelta64(np.iinfo(np.int64).max, "D") + result = constructor(scalar) + item = get1(result) + assert type(item) is np.timedelta64 + dtype = result.dtype if isinstance(result, Series) else result.dtypes.iloc[0] + assert dtype == object def test_tzaware_data_tznaive_dtype(self, constructor): tz = "US/Eastern" diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 25f75390b9d6c..28d2ffa6946cf 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -5,6 +5,7 @@ import pytest from pandas._libs.tslibs import OutOfBoundsTimedelta +from pandas._libs.tslibs.dtypes import NpyDatetimeUnit from pandas import ( NaT, @@ -26,18 +27,40 @@ def test_construct_with_weeks_unit_overflow(): def test_construct_from_td64_with_unit(): # ignore the unit, as it may cause silently overflows leading to incorrect # results, and in non-overflow cases is irrelevant GH#46827 - obj = np.timedelta64(123456789, "h") + obj = np.timedelta64(123456789000000000, "h") - with pytest.raises(OutOfBoundsTimedelta, match="123456789 hours"): + with pytest.raises(OutOfBoundsTimedelta, match="123456789000000000 hours"): Timedelta(obj, unit="ps") - with pytest.raises(OutOfBoundsTimedelta, match="123456789 hours"): + with pytest.raises(OutOfBoundsTimedelta, match="123456789000000000 hours"): Timedelta(obj, unit="ns") - with pytest.raises(OutOfBoundsTimedelta, match="123456789 hours"): + with pytest.raises(OutOfBoundsTimedelta, match="123456789000000000 hours"): Timedelta(obj) +def test_from_td64_retain_resolution(): + # case where we retain millisecond resolution + obj = np.timedelta64(12345, "ms") + + td = Timedelta(obj) + assert td.value == obj.view("i8") + assert td._reso == NpyDatetimeUnit.NPY_FR_ms.value + + # Case where we cast to nearest-supported reso + obj2 = np.timedelta64(1234, "D") + td2 = Timedelta(obj2) + assert td2._reso == NpyDatetimeUnit.NPY_FR_s.value + assert td2 == obj2 + assert td2.days == 1234 + + # Case that _would_ overflow if we didn't support non-nano + obj3 = np.timedelta64(1000000000000000000, "us") + td3 = Timedelta(obj3) + assert td3.total_seconds() == 1000000000000 + assert td3._reso == NpyDatetimeUnit.NPY_FR_us.value + + def test_construction(): expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8") assert Timedelta(10, unit="d").value == expected @@ -231,17 +254,17 @@ def test_overflow_on_construction(): @pytest.mark.parametrize( - "val, unit, name", + "val, unit", [ - (3508, "M", " months"), - (15251, "W", " weeks"), # 1 - (106752, "D", " days"), # change from previous: - (2562048, "h", " hours"), # 0 hours - (153722868, "m", " minutes"), # 13 minutes - (9223372037, "s", " seconds"), # 44 seconds + (3508, "M"), + (15251, "W"), # 1 + (106752, "D"), # change from previous: + (2562048, "h"), # 0 hours + (153722868, "m"), # 13 minutes + (9223372037, "s"), # 44 seconds ], ) -def test_construction_out_of_bounds_td64(val, unit, name): +def test_construction_out_of_bounds_td64ns(val, unit): # TODO: parametrize over units just above/below the implementation bounds # once GH#38964 is resolved @@ -249,9 +272,15 @@ def test_construction_out_of_bounds_td64(val, unit, name): td64 = np.timedelta64(val, unit) assert td64.astype("m8[ns]").view("i8") < 0 # i.e. naive astype will be wrong - msg = str(val) + name + td = Timedelta(td64) + if unit != "M": + # with unit="M" the conversion to "s" is poorly defined + # (and numpy issues DeprecationWarning) + assert td.asm8 == td64 + assert td.asm8.dtype == "m8[s]" + msg = r"Cannot cast 1067\d\d days .* to unit='ns' without overflow" with pytest.raises(OutOfBoundsTimedelta, match=msg): - Timedelta(td64) + td._as_unit("ns") # But just back in bounds and we are OK assert Timedelta(td64 - 1) == td64 - 1 @@ -259,13 +288,34 @@ def test_construction_out_of_bounds_td64(val, unit, name): td64 *= -1 assert td64.astype("m8[ns]").view("i8") > 0 # i.e. naive astype will be wrong - with pytest.raises(OutOfBoundsTimedelta, match="-" + msg): - Timedelta(td64) + td2 = Timedelta(td64) + msg = r"Cannot cast -1067\d\d days .* to unit='ns' without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=msg): + td2._as_unit("ns") # But just back in bounds and we are OK assert Timedelta(td64 + 1) == td64 + 1 +@pytest.mark.parametrize( + "val, unit", + [ + (3508 * 10**9, "M"), + (15251 * 10**9, "W"), + (106752 * 10**9, "D"), + (2562048 * 10**9, "h"), + (153722868 * 10**9, "m"), + ], +) +def test_construction_out_of_bounds_td64s(val, unit): + td64 = np.timedelta64(val, unit) + with pytest.raises(OutOfBoundsTimedelta, match=str(td64)): + Timedelta(td64) + + # But just back in bounds and we are OK + assert Timedelta(td64 - 10**9) == td64 - 10**9 + + @pytest.mark.parametrize( "fmt,exp", [ diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index 8d3738d40601b..f865c5d0cec3b 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -183,7 +183,7 @@ def test_truediv_timedeltalike(self, td): assert (2.5 * td) / td == 2.5 other = Timedelta(td.value) - msg = "with mismatched resolutions are not supported" + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow." with pytest.raises(ValueError, match=msg): td / other @@ -207,7 +207,7 @@ def test_floordiv_timedeltalike(self, td): assert (2.5 * td) // td == 2 other = Timedelta(td.value) - msg = "with mismatched resolutions are not supported" + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" with pytest.raises(ValueError, match=msg): td // other @@ -259,15 +259,14 @@ def test_addsub_mismatched_reso(self, td): assert result.days == 1 - td.days other2 = Timedelta(500) - # TODO: should be OutOfBoundsTimedelta - msg = "value too large" - with pytest.raises(OverflowError, match=msg): + msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" + with pytest.raises(OutOfBoundsTimedelta, match=msg): td + other2 - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): other2 + td - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): td - other2 - with pytest.raises(OverflowError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): other2 - td def test_min(self, td): From 5030951eea62f468adb92d1e90edd44f4ddeb0e7 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 2 Oct 2022 14:26:24 -0700 Subject: [PATCH 5/7] API: Timedelta constructor pytimedelta, Tick preserve reso --- pandas/_libs/tslibs/timedeltas.pyx | 20 +++++++--- pandas/core/arrays/masked.py | 12 +++++- pandas/core/arrays/timedeltas.py | 4 +- pandas/tests/arithmetic/test_numeric.py | 19 +++++++++ pandas/tests/dtypes/cast/test_promote.py | 6 +++ pandas/tests/io/json/test_pandas.py | 8 ++-- .../scalar/timedelta/test_constructors.py | 39 +++++++++++++++++-- .../tests/scalar/timedelta/test_timedelta.py | 19 +++++---- 8 files changed, 104 insertions(+), 23 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index ee04750f337f1..38c2feecf7c88 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -341,8 +341,9 @@ cdef convert_to_timedelta64(object ts, str unit): elif isinstance(ts, _Timedelta): # already in the proper format if ts._reso != NPY_FR_ns: - raise NotImplementedError - ts = np.timedelta64(ts.value, "ns") + ts = ts._as_unit("ns").asm8 + else: + ts = np.timedelta64(ts.value, "ns") elif is_timedelta64_object(ts): ts = ensure_td64ns(ts) elif is_integer_object(ts): @@ -1698,7 +1699,13 @@ class Timedelta(_Timedelta): value = parse_timedelta_string(value) value = np.timedelta64(value) elif PyDelta_Check(value): - value = convert_to_timedelta64(value, 'ns') + # pytimedelta object -> microsecond resolution + new_value = delta_to_nanoseconds( + value, reso=NPY_DATETIMEUNIT.NPY_FR_us + ) + return cls._from_value_and_reso( + new_value, reso=NPY_DATETIMEUNIT.NPY_FR_us + ) elif is_timedelta64_object(value): # Retain the resolution if possible, otherwise cast to the nearest # supported resolution. @@ -1712,7 +1719,7 @@ class Timedelta(_Timedelta): if reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC: try: new_value = convert_reso( - get_timedelta64_value(value), + new_value, reso, new_reso, round_ok=True, @@ -1722,7 +1729,10 @@ class Timedelta(_Timedelta): return cls._from_value_and_reso(new_value, reso=new_reso) elif is_tick_object(value): - value = np.timedelta64(value.nanos, 'ns') + new_reso = get_supported_reso(value._reso) + new_value = delta_to_nanoseconds(value, reso=new_reso) + return cls._from_value_and_reso(new_value, reso=new_reso) + elif is_integer_object(value) or is_float_object(value): # unit=None is de-facto 'ns' unit = parse_timedelta_unit(unit) diff --git a/pandas/core/arrays/masked.py b/pandas/core/arrays/masked.py index 043e0baf3ec0e..dcfb9ad6cc667 100644 --- a/pandas/core/arrays/masked.py +++ b/pandas/core/arrays/masked.py @@ -17,6 +17,10 @@ lib, missing as libmissing, ) +from pandas._libs.tslibs import ( + get_unit_from_dtype, + is_supported_unit, +) from pandas._typing import ( ArrayLike, AstypeArg, @@ -750,12 +754,16 @@ def _maybe_mask_result(self, result, mask): return BooleanArray(result, mask, copy=False) - elif result.dtype == "timedelta64[ns]": + elif ( + isinstance(result.dtype, np.dtype) + and result.dtype.kind == "m" + and is_supported_unit(get_unit_from_dtype(result.dtype)) + ): # e.g. test_numeric_arr_mul_tdscalar_numexpr_path from pandas.core.arrays import TimedeltaArray if not isinstance(result, TimedeltaArray): - result = TimedeltaArray._simple_new(result) + result = TimedeltaArray._simple_new(result, dtype=result.dtype) result[mask] = result.dtype.type("NaT") return result diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 2c97ce2fce242..a0fbe866fc964 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -257,10 +257,10 @@ def _generate_range(cls, start, end, periods, freq, closed=None): ) if start is not None: - start = Timedelta(start) + start = Timedelta(start)._as_unit("ns") if end is not None: - end = Timedelta(end) + end = Timedelta(end)._as_unit("ns") left_closed, right_closed = validate_endpoints(closed) diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index f63e70ced4b52..f3941ad04ad84 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import abc +from datetime import timedelta from decimal import Decimal import operator from typing import Any @@ -27,6 +28,7 @@ Int64Index, UInt64Index, ) +from pandas.core.arrays import TimedeltaArray from pandas.core.computation import expressions as expr from pandas.tests.arithmetic.common import ( assert_invalid_addsub_type, @@ -209,6 +211,11 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box_with_array): tda = expected._data dtype = scalar_td.dtype expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) + elif type(scalar_td) is timedelta and box not in [Index, Series]: + # TODO(2.0): once TDA.astype converts to m8, just do expected.astype + tda = expected._data + dtype = np.dtype("m8[us]") + expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) @@ -240,6 +247,13 @@ def test_numeric_arr_mul_tdscalar_numexpr_path( obj = tm.box_expected(arr, box, transpose=False) expected = arr_i8.view("timedelta64[D]").astype("timedelta64[ns]") + if type(scalar_td) is timedelta and box is array: + # TODO(2.0): this shouldn't depend on 'box' + expected = expected.astype("timedelta64[us]") + # TODO(2.0): won't be necessary to construct TimedeltaArray + # explicitly. + expected = TimedeltaArray._simple_new(expected, dtype=expected.dtype) + expected = tm.box_expected(expected, box, transpose=False) result = obj * scalar_td @@ -262,6 +276,11 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box_with_array # i.e. resolution is lower -> use lowest supported resolution dtype = np.dtype("m8[s]") expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) + elif type(three_days) is timedelta and box not in [Index, Series]: + # TODO(2.0): just use expected.astype + tda = expected._data + dtype = np.dtype("m8[us]") + expected = type(tda)._simple_new(tda._ndarray.astype(dtype), dtype=dtype) index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) diff --git a/pandas/tests/dtypes/cast/test_promote.py b/pandas/tests/dtypes/cast/test_promote.py index c07e0a187babe..bfecbbbfc0435 100644 --- a/pandas/tests/dtypes/cast/test_promote.py +++ b/pandas/tests/dtypes/cast/test_promote.py @@ -480,6 +480,12 @@ def test_maybe_promote_any_with_timedelta64( "Timedelta scalar" ) request.node.add_marker(mark) + elif type(fill_value) is datetime.timedelta: + mark = pytest.mark.xfail( + reason="maybe_promote not yet updated to handle non-nano " + "Timedelta scalar" + ) + request.node.add_marker(mark) else: expected_dtype = np.dtype(object) exp_val_for_scalar = fill_value diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index 026c3bc68ce34..daa8550965db4 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -1039,11 +1039,11 @@ def test_timedelta(self): tm.assert_frame_equal(frame, result) def test_mixed_timedelta_datetime(self): - frame = DataFrame({"a": [timedelta(23), Timestamp("20130101")]}, dtype=object) + td = timedelta(23) + ts = Timestamp("20130101") + frame = DataFrame({"a": [td, ts]}, dtype=object) - expected = DataFrame( - {"a": [pd.Timedelta(frame.a[0]).value, Timestamp(frame.a[1]).value]} - ) + expected = DataFrame({"a": [pd.Timedelta(td)._as_unit("ns").value, ts.value]}) result = read_json(frame.to_json(date_unit="ns"), dtype={"a": "int64"}) tm.assert_frame_equal(result, expected, check_index_type=False) diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 28d2ffa6946cf..8840dab34e8cf 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -61,6 +61,38 @@ def test_from_td64_retain_resolution(): assert td3._reso == NpyDatetimeUnit.NPY_FR_us.value +def test_from_pytimedelta_us_reso(): + # pytimedelta has microsecond resolution, so Timedelta(pytd) inherits that + td = timedelta(days=4, minutes=3) + result = Timedelta(td) + assert result.to_pytimedelta() == td + assert result._reso == NpyDatetimeUnit.NPY_FR_us.value + + +def test_from_tick_reso(): + tick = offsets.Nano() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_ns.value + + tick = offsets.Micro() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_us.value + + tick = offsets.Milli() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_ms.value + + tick = offsets.Second() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value + + # everything above Second gets cast to the closest supported reso: second + tick = offsets.Minute() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value + + tick = offsets.Hour() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value + + tick = offsets.Day() + assert Timedelta(tick)._reso == NpyDatetimeUnit.NPY_FR_s.value + + def test_construction(): expected = np.timedelta64(10, "D").astype("m8[ns]").view("i8") assert Timedelta(10, unit="d").value == expected @@ -248,9 +280,10 @@ def test_overflow_on_construction(): with pytest.raises(OutOfBoundsTimedelta, match=msg): Timedelta(7 * 19999, unit="D") - msg = "Cannot cast 259987 days, 0:00:00 to unit=ns without overflow" - with pytest.raises(OutOfBoundsTimedelta, match=msg): - Timedelta(timedelta(days=13 * 19999)) + # used to overflow before non-ns support + td = Timedelta(timedelta(days=13 * 19999)) + assert td._reso == NpyDatetimeUnit.NPY_FR_us.value + assert td.days == 13 * 19999 @pytest.mark.parametrize( diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py index f865c5d0cec3b..d61350bf1c2e4 100644 --- a/pandas/tests/scalar/timedelta/test_timedelta.py +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -187,9 +187,12 @@ def test_truediv_timedeltalike(self, td): with pytest.raises(ValueError, match=msg): td / other - with pytest.raises(ValueError, match=msg): - # __rtruediv__ - other.to_pytimedelta() / td + # Timedelta(other.to_pytimedelta()) has microsecond resolution, + # so the division doesn't require casting all the way to nanos, + # so succeeds + res = other.to_pytimedelta() / td + expected = other.to_pytimedelta() / td.to_pytimedelta() + assert res == expected def test_truediv_numeric(self, td): assert td / np.nan is NaT @@ -208,12 +211,14 @@ def test_floordiv_timedeltalike(self, td): other = Timedelta(td.value) msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow" - with pytest.raises(ValueError, match=msg): + with pytest.raises(OutOfBoundsTimedelta, match=msg): td // other - with pytest.raises(ValueError, match=msg): - # __rfloordiv__ - other.to_pytimedelta() // td + # Timedelta(other.to_pytimedelta()) has microsecond resolution, + # so the floordiv doesn't require casting all the way to nanos, + # so succeeds + res = other.to_pytimedelta() // td + assert res == 0 def test_floordiv_numeric(self, td): assert td // np.nan is NaT From 3e4827c56d0e504c609b1566711058ab1fa47d1c Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 7 Oct 2022 10:33:48 -0700 Subject: [PATCH 6/7] remove debugging variable --- pandas/_libs/tslibs/timedeltas.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 27b4724281bd4..dd19306bd49c6 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1926,7 +1926,6 @@ class Timedelta(_Timedelta): def __floordiv__(self, other): # numpy does not implement floordiv for timedelta64 dtype, so we cannot # just defer - orig = other if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) From 880e9dcaa5133fcb4854b29911b3eaa665b2e896 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 7 Oct 2022 10:34:28 -0700 Subject: [PATCH 7/7] remove duplicate --- pandas/core/construction.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pandas/core/construction.py b/pandas/core/construction.py index c3479a27f001f..88e82a67ab854 100644 --- a/pandas/core/construction.py +++ b/pandas/core/construction.py @@ -325,13 +325,6 @@ def array( data = extract_array(data, extract_numpy=True) - if isinstance(data, ExtensionArray) and ( - dtype is None or is_dtype_equal(dtype, data.dtype) - ): - if copy: - return data.copy() - return data - # this returns None for not-found dtypes. if isinstance(dtype, str): dtype = registry.find(dtype) or dtype