Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 4 additions & 43 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,7 @@ def _add_datetimelike_scalar(self, other) -> DatetimeArray:

dtype = tz_to_dtype(tz=other.tz, unit=self.unit)
res_values = result.view(f"M8[{self.unit}]")
return DatetimeArray._simple_new(res_values, dtype=dtype, freq=None)
return DatetimeArray._simple_new(res_values, dtype=dtype)

@final
def _add_datetime_arraylike(self, other: DatetimeArray) -> DatetimeArray:
Expand Down Expand Up @@ -1165,7 +1165,7 @@ def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray:
res_values = add_overflowsafe(self.asi8, np.asarray(-other_i8, dtype="i8"))
res_m8 = res_values.view(f"timedelta64[{self.unit}]")

return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype, freq=None)
return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype)

@final
def _add_period(self, other: Period) -> PeriodArray:
Expand Down Expand Up @@ -1227,12 +1227,7 @@ def _add_timedeltalike(self, other: Timedelta | TimedeltaArray) -> Self:
new_values = add_overflowsafe(self.asi8, np.asarray(other_i8, dtype="i8"))
res_values = new_values.view(self._ndarray.dtype)

# error: Unexpected keyword argument "freq" for "_simple_new" of "NDArrayBacked"
return type(self)._simple_new(
res_values,
dtype=self.dtype,
freq=None, # type: ignore[call-arg]
)
return type(self)._simple_new(res_values, dtype=self.dtype)

@final
def _add_nat(self) -> Self:
Expand All @@ -1249,12 +1244,7 @@ def _add_nat(self) -> Self:
result = np.empty(self.shape, dtype=np.int64)
result.fill(iNaT)
result = result.view(self._ndarray.dtype) # preserve reso
# error: Unexpected keyword argument "freq" for "_simple_new" of "NDArrayBacked"
return type(self)._simple_new(
result,
dtype=self.dtype,
freq=None, # type: ignore[call-arg]
)
return type(self)._simple_new(result, dtype=self.dtype)

@final
def _sub_nat(self) -> np.ndarray:
Expand Down Expand Up @@ -2386,35 +2376,6 @@ def all(self, *, axis: AxisInt | None = None, skipna: bool = True) -> bool:
def _maybe_clear_freq(self) -> None:
self._freq = None

def _with_freq(self, freq) -> Self:
"""
Helper to get a view on the same data, with a new freq.

Parameters
----------
freq : DateOffset, None, or "infer"

Returns
-------
Same type as self
"""
# GH#29843
if freq is None:
# Always valid
pass
elif isinstance(freq, BaseOffset):
# In the TimedeltaArray case, we require a Tick offset
if self.dtype.kind == "m" and not isinstance(freq, (Tick, Day)):
raise TypeError("TimedeltaArray/Index freq must be a Tick")
elif freq == "infer":
freq = to_offset(self.inferred_freq)
else:
raise ValueError(f"Invalid frequency: {freq!r}")

arr = self.view()
arr._freq = freq
return arr

# --------------------------------------------------------------
# ExtensionArray Interface

Expand Down
4 changes: 2 additions & 2 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ def tz_convert(self, tz) -> Self:

# No conversion since timestamps are all UTC to begin with
dtype = tz_to_dtype(tz, unit=self.unit)
return self._simple_new(self._ndarray, dtype=dtype, freq=None)
return self._simple_new(self._ndarray, dtype=dtype)

@dtl.ravel_compat
def tz_localize(
Expand Down Expand Up @@ -1122,7 +1122,7 @@ def tz_localize(
new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
dtype = tz_to_dtype(tz, unit=self.unit)

return self._simple_new(new_dates_dt64, dtype=dtype, freq=None)
return self._simple_new(new_dates_dt64, dtype=dtype)

# ----------------------------------------------------------------
# Conversion Methods - Vectorized analogues of Timestamp methods
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray:
# TODO: other cases?
return dta
else:
dta = dta._with_freq("infer")
dta._freq = to_offset(dta.inferred_freq)
if freq is not None:
freq = to_offset(freq)
if (
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ def _accumulate(self, name: str, *, skipna: bool = True, **kwargs):
op = getattr(datetimelike_accumulations, name)
result = op(self._ndarray.copy(), skipna=skipna, **kwargs)

return type(self)._simple_new(result, freq=None, dtype=self.dtype)
return type(self)._simple_new(result, dtype=self.dtype)
elif name == "cumprod":
raise TypeError("cumprod not supported for Timedelta.")

Expand Down
23 changes: 13 additions & 10 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,16 +917,18 @@ def as_unit(self, unit: TimeUnit) -> Self:

def _with_freq(self, freq):
# GH#29843
if freq is None or (len(self) == 0 and isinstance(freq, BaseOffset)):
# None is always valid. For offsets on empty index the array's
# _with_freq below performs the m-dtype Tick validation.
if freq is None:
pass
else:
# As an internal method, we can ensure this assertion always holds
assert freq == "infer"
elif isinstance(freq, BaseOffset):
if self.dtype.kind == "m" and not isinstance(freq, (Tick, Day)):
raise TypeError("TimedeltaArray/Index freq must be a Tick")
elif freq == "infer":
freq = to_offset(self.inferred_freq)
else:
raise ValueError(f"Invalid frequency: {freq!r}")

arr = self._data._with_freq(freq)
arr = self._data.view()
arr._freq = freq
return type(self)._simple_new(arr, name=self._name)

@property
Expand Down Expand Up @@ -1218,12 +1220,13 @@ def _union(self, other, sort):
return self._range_union(other, sort=sort)

if self._can_fast_union(other):
result = self._fast_union(other, sort=sort)
# in the case with sort=None, the _can_fast_union check ensures
# that result.freq == self.freq
return result
return self._fast_union(other, sort=sort)
else:
return super()._union(other, sort)._with_freq("infer") # type: ignore[union-attr]
# super()._union can return an ArrayLike; wrap into an Index first
result = self._wrap_setop_result(other, super()._union(other, sort))
return result._with_freq("infer") # type: ignore[attr-defined]

# --------------------------------------------------------------------
# Join Methods
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def normalize(self) -> Self:
dtype='datetime64[us, Asia/Calcutta]', freq=None)
"""
arr = self._data.normalize()
arr = arr._with_freq("infer")
arr._freq = to_offset(arr.inferred_freq)
return type(self)._simple_new(arr, name=self.name)

def tz_convert(self, tz) -> Self:
Expand Down
3 changes: 2 additions & 1 deletion pandas/core/internals/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2252,7 +2252,8 @@ def maybe_coerce_values(values: ArrayLike) -> ArrayLike:

if isinstance(values, (DatetimeArray, TimedeltaArray)) and values.freq is not None:
# freq is only stored in DatetimeIndex/TimedeltaIndex, not in Series/DataFrame
values = values._with_freq(None)
values = values.view()
values._freq = None

return values

Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/arithmetic/test_timedelta64.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ def test_td64arr_add_sub_datetimelike_scalar(
if box_with_array is pd.array:
# GH#24566 Array-level __neg__ and __rsub__ don't carry freq;
# freq is managed by the Index layer.
expected2 = expected2._with_freq(None)
expected2._freq = None

tm.assert_equal(ts - tdarr, expected2)
tm.assert_equal(ts + (-tdarr), expected2)
Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/arrays/datetimes/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def test_mixing_naive_tzaware_raises(self, meth):
def test_from_pandas_array(self):
arr = pd.array(np.arange(5, dtype=np.int64)) * 3600 * 10**9

result = DatetimeArray._from_sequence(arr, dtype="M8[ns]")._with_freq("infer")
result = DatetimeArray._from_sequence(arr, dtype="M8[ns]")
result._freq = pd.tseries.frequencies.to_offset(result.inferred_freq)

expected = pd.date_range("1970-01-01", periods=5, freq="h", unit="ns")._data
tm.assert_datetime_array_equal(result, expected)
Expand Down
8 changes: 6 additions & 2 deletions pandas/tests/arrays/datetimes/test_cumulative.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import pandas._testing as tm
from pandas.core.arrays import DatetimeArray

from pandas.tseries.frequencies import to_offset


class TestAccumulator:
def test_accumulators_freq(self):
Expand All @@ -14,7 +16,8 @@ def test_accumulators_freq(self):
"2000-01-03",
],
dtype="M8[ns]",
)._with_freq("infer")
)
arr._freq = to_offset(arr.inferred_freq)
result = arr._accumulate("cummin")
expected = DatetimeArray._from_sequence(["2000-01-01"] * 3, dtype="M8[ns]")
tm.assert_datetime_array_equal(result, expected)
Expand All @@ -39,6 +42,7 @@ def test_accumulators_disallowed(self, func):
"2000-01-02",
],
dtype="M8[ns]",
)._with_freq("infer")
)
arr._freq = to_offset(arr.inferred_freq)
with pytest.raises(TypeError, match=f"Accumulation {func}"):
arr._accumulate(func)
7 changes: 4 additions & 3 deletions pandas/tests/arrays/test_datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def test_setitem_list_of_nats(self, arr1d):
def test_setitem_object_dtype(self, box, arr1d):
expected = arr1d.copy()[::-1]
if expected.dtype.kind in ["m", "M"]:
expected = expected._with_freq(None)
expected._freq = None

vals = expected
if box is list:
Expand Down Expand Up @@ -496,7 +496,7 @@ def test_setitem_strs(self, arr1d):
def test_setitem_categorical(self, arr1d, as_index):
expected = arr1d.copy()[::-1]
if not isinstance(expected, PeriodArray):
expected = expected._with_freq(None)
expected._freq = None

cat = pd.Categorical(arr1d)
if as_index:
Expand Down Expand Up @@ -652,7 +652,8 @@ def test_round(self, arr1d):

dta = dti._data
result = dta.round(freq="2min")
expected = expected._data._with_freq(None)
expected = expected._data.view()
expected._freq = None
tm.assert_datetime_array_equal(result, expected)

def test_array_interface(self, datetime_index):
Expand Down
3 changes: 2 additions & 1 deletion pandas/tests/extension/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ def test_reduce_series_boolean(self, data, all_boolean_reductions, skipna):

def test_series_constructor(self, data):
# Series construction drops any .freq attr
data = data._with_freq(None)
data = data.view()
data._freq = None
super().test_series_constructor(data)

@pytest.mark.parametrize("na_action", [None, "ignore"])
Expand Down
7 changes: 5 additions & 2 deletions pandas/tests/indexes/timedeltas/methods/test_astype.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,14 @@ def test_astype_freq_conversion(self):

# check this matches Series and TimedeltaArray
# freq is not preserved at the array level; Index handles freq
exp_arr = expected._values.view()
exp_arr._freq = None

res = tdi._data.astype("m8[s]")
tm.assert_equal(res, expected._values._with_freq(None))
tm.assert_equal(res, exp_arr)

res = tdi.to_series().astype("m8[s]")
tm.assert_equal(res._values, expected._values._with_freq(None))
tm.assert_equal(res._values, exp_arr)

@pytest.mark.parametrize("dtype", [float, "datetime64", "datetime64[ns]"])
def test_astype_raises(self, dtype):
Expand Down
2 changes: 0 additions & 2 deletions pandas/tests/indexes/timedeltas/test_freq_attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ def test_with_freq_empty_requires_tick(self):
msg = "TimedeltaArray/Index freq must be a Tick"
with pytest.raises(TypeError, match=msg):
idx._with_freq(off)
with pytest.raises(TypeError, match=msg):
idx._data._with_freq(off)

def test_freq_setter_errors(self):
# GH#20678
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/tools/test_to_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2783,7 +2783,7 @@ def test_to_datetime_dta_tz(self, klass):
result = to_datetime(obj, utc=True)
if klass is not DatetimeIndex:
# Array methods no longer set freq; freq is managed by Index
expected = expected._with_freq(None)
expected._freq = None
tm.assert_equal(result, expected)


Expand Down
Loading