Skip to content

Commit 8335eb7

Browse files
authored
Merge branch 'main' into cow_indexing_midx_slice
2 parents ed83c88 + 7ab6f8b commit 8335eb7

18 files changed

+178
-65
lines changed

doc/source/whatsnew/v2.0.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ Copy-on-Write improvements
209209
- :meth:`DataFrame.__getitem__` will now respect the Copy-on-Write mechanism when the
210210
:class:`DataFrame` has :class:`MultiIndex` columns.
211211

212+
- :meth:`Series.view` will now respect the Copy-on-Write mechanism.
213+
212214
Copy-on-Write can be enabled through one of
213215

214216
.. code-block:: python

doc/source/whatsnew/v2.1.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ Groupby/resample/rolling
201201
^^^^^^^^^^^^^^^^^^^^^^^^
202202
- Bug in :meth:`DataFrameGroupBy.idxmin`, :meth:`SeriesGroupBy.idxmin`, :meth:`DataFrameGroupBy.idxmax`, :meth:`SeriesGroupBy.idxmax` return wrong dtype when used on empty DataFrameGroupBy or SeriesGroupBy (:issue:`51423`)
203203
- Bug in weighted rolling aggregations when specifying ``min_periods=0`` (:issue:`51449`)
204+
- Bug in :meth:`DataFrame.resample` and :meth:`Series.resample` in incorrectly allowing non-fixed ``freq`` when resampling on a :class:`TimedeltaIndex` (:issue:`51896`)
204205
-
205206

206207
Reshaping
@@ -227,6 +228,7 @@ Styler
227228

228229
Other
229230
^^^^^
231+
-
230232

231233
.. ***DO NOT USE THIS SECTION***
232234

environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,4 @@ dependencies:
118118

119119
- pip:
120120
- sphinx-toggleprompt
121+
- typing_extensions; python_version<"3.11"

pandas/_libs/sparse.pyi

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
from typing import (
2-
Sequence,
3-
TypeVar,
4-
)
1+
from typing import Sequence
52

63
import numpy as np
74

8-
from pandas._typing import npt
9-
10-
_SparseIndexT = TypeVar("_SparseIndexT", bound=SparseIndex)
5+
from pandas._typing import (
6+
Self,
7+
npt,
8+
)
119

1210
class SparseIndex:
1311
length: int
@@ -24,8 +22,8 @@ class SparseIndex:
2422
def lookup_array(self, indexer: npt.NDArray[np.int32]) -> npt.NDArray[np.int32]: ...
2523
def to_int_index(self) -> IntIndex: ...
2624
def to_block_index(self) -> BlockIndex: ...
27-
def intersect(self: _SparseIndexT, y_: SparseIndex) -> _SparseIndexT: ...
28-
def make_union(self: _SparseIndexT, y_: SparseIndex) -> _SparseIndexT: ...
25+
def intersect(self, y_: SparseIndex) -> Self: ...
26+
def make_union(self, y_: SparseIndex) -> Self: ...
2927

3028
class IntIndex(SparseIndex):
3129
indices: npt.NDArray[np.int32]

pandas/_libs/tslibs/offsets.pyi

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ from typing import (
1313
import numpy as np
1414

1515
from pandas._libs.tslibs.nattype import NaTType
16-
from pandas._typing import npt
16+
from pandas._typing import (
17+
Self,
18+
npt,
19+
)
1720

1821
from .timedeltas import Timedelta
1922

@@ -39,40 +42,40 @@ class BaseOffset:
3942
@overload
4043
def __add__(self, other: npt.NDArray[np.object_]) -> npt.NDArray[np.object_]: ...
4144
@overload
42-
def __add__(self: _BaseOffsetT, other: BaseOffset) -> _BaseOffsetT: ...
45+
def __add__(self, other: BaseOffset) -> Self: ...
4346
@overload
4447
def __add__(self, other: _DatetimeT) -> _DatetimeT: ...
4548
@overload
4649
def __add__(self, other: _TimedeltaT) -> _TimedeltaT: ...
4750
@overload
4851
def __radd__(self, other: npt.NDArray[np.object_]) -> npt.NDArray[np.object_]: ...
4952
@overload
50-
def __radd__(self: _BaseOffsetT, other: BaseOffset) -> _BaseOffsetT: ...
53+
def __radd__(self, other: BaseOffset) -> Self: ...
5154
@overload
5255
def __radd__(self, other: _DatetimeT) -> _DatetimeT: ...
5356
@overload
5457
def __radd__(self, other: _TimedeltaT) -> _TimedeltaT: ...
5558
@overload
5659
def __radd__(self, other: NaTType) -> NaTType: ...
57-
def __sub__(self: _BaseOffsetT, other: BaseOffset) -> _BaseOffsetT: ...
60+
def __sub__(self, other: BaseOffset) -> Self: ...
5861
@overload
5962
def __rsub__(self, other: npt.NDArray[np.object_]) -> npt.NDArray[np.object_]: ...
6063
@overload
61-
def __rsub__(self: _BaseOffsetT, other: BaseOffset) -> _BaseOffsetT: ...
64+
def __rsub__(self, other: BaseOffset): ...
6265
@overload
6366
def __rsub__(self, other: _DatetimeT) -> _DatetimeT: ...
6467
@overload
6568
def __rsub__(self, other: _TimedeltaT) -> _TimedeltaT: ...
6669
@overload
6770
def __mul__(self, other: np.ndarray) -> np.ndarray: ...
6871
@overload
69-
def __mul__(self: _BaseOffsetT, other: int) -> _BaseOffsetT: ...
72+
def __mul__(self, other: int): ...
7073
@overload
7174
def __rmul__(self, other: np.ndarray) -> np.ndarray: ...
7275
@overload
73-
def __rmul__(self: _BaseOffsetT, other: int) -> _BaseOffsetT: ...
74-
def __neg__(self: _BaseOffsetT) -> _BaseOffsetT: ...
75-
def copy(self: _BaseOffsetT) -> _BaseOffsetT: ...
76+
def __rmul__(self, other: int) -> Self: ...
77+
def __neg__(self) -> Self: ...
78+
def copy(self) -> Self: ...
7679
@property
7780
def name(self) -> str: ...
7881
@property

pandas/_libs/tslibs/timedeltas.pyi

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ from pandas._libs.tslibs import (
1212
NaTType,
1313
Tick,
1414
)
15-
from pandas._typing import npt
15+
from pandas._typing import (
16+
Self,
17+
npt,
18+
)
1619

1720
# This should be kept consistent with the keys in the dict timedelta_abbrevs
1821
# in pandas/_libs/tslibs/timedeltas.pyx
@@ -111,9 +114,9 @@ class Timedelta(timedelta):
111114
@property
112115
def asm8(self) -> np.timedelta64: ...
113116
# TODO: round/floor/ceil could return NaT?
114-
def round(self: _S, freq: str) -> _S: ...
115-
def floor(self: _S, freq: str) -> _S: ...
116-
def ceil(self: _S, freq: str) -> _S: ...
117+
def round(self, freq: str) -> Self: ...
118+
def floor(self, freq: str) -> Self: ...
119+
def ceil(self, freq: str) -> Self: ...
117120
@property
118121
def resolution_string(self) -> str: ...
119122
def __add__(self, other: timedelta) -> Timedelta: ...

pandas/_libs/tslibs/timestamps.pyi

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ from pandas._libs.tslibs import (
2121
Tick,
2222
Timedelta,
2323
)
24+
from pandas._typing import Self
2425

2526
_DatetimeT = TypeVar("_DatetimeT", bound=datetime)
2627

@@ -80,30 +81,28 @@ class Timestamp(datetime):
8081
@property
8182
def fold(self) -> int: ...
8283
@classmethod
83-
def fromtimestamp(
84-
cls: type[_DatetimeT], ts: float, tz: _tzinfo | None = ...
85-
) -> _DatetimeT: ...
84+
def fromtimestamp(cls, ts: float, tz: _tzinfo | None = ...) -> Self: ...
8685
@classmethod
87-
def utcfromtimestamp(cls: type[_DatetimeT], ts: float) -> _DatetimeT: ...
86+
def utcfromtimestamp(cls, ts: float) -> Self: ...
8887
@classmethod
89-
def today(cls: type[_DatetimeT], tz: _tzinfo | str | None = ...) -> _DatetimeT: ...
88+
def today(cls, tz: _tzinfo | str | None = ...) -> Self: ...
9089
@classmethod
9190
def fromordinal(
92-
cls: type[_DatetimeT],
91+
cls,
9392
ordinal: int,
9493
tz: _tzinfo | str | None = ...,
95-
) -> _DatetimeT: ...
94+
) -> Self: ...
9695
@classmethod
97-
def now(cls: type[_DatetimeT], tz: _tzinfo | str | None = ...) -> _DatetimeT: ...
96+
def now(cls, tz: _tzinfo | str | None = ...) -> Self: ...
9897
@classmethod
99-
def utcnow(cls: type[_DatetimeT]) -> _DatetimeT: ...
98+
def utcnow(cls) -> Self: ...
10099
# error: Signature of "combine" incompatible with supertype "datetime"
101100
@classmethod
102101
def combine( # type: ignore[override]
103102
cls, date: _date, time: _time
104103
) -> datetime: ...
105104
@classmethod
106-
def fromisoformat(cls: type[_DatetimeT], date_string: str) -> _DatetimeT: ...
105+
def fromisoformat(cls, date_string: str) -> Self: ...
107106
def strftime(self, format: str) -> str: ...
108107
def __format__(self, fmt: str) -> str: ...
109108
def toordinal(self) -> int: ...
@@ -116,7 +115,7 @@ class Timestamp(datetime):
116115
# LSP violation: nanosecond is not present in datetime.datetime.replace
117116
# and has positional args following it
118117
def replace( # type: ignore[override]
119-
self: _DatetimeT,
118+
self,
120119
year: int | None = ...,
121120
month: int | None = ...,
122121
day: int | None = ...,
@@ -127,11 +126,9 @@ class Timestamp(datetime):
127126
nanosecond: int | None = ...,
128127
tzinfo: _tzinfo | type[object] | None = ...,
129128
fold: int | None = ...,
130-
) -> _DatetimeT: ...
129+
) -> Self: ...
131130
# LSP violation: datetime.datetime.astimezone has a default value for tz
132-
def astimezone( # type: ignore[override]
133-
self: _DatetimeT, tz: _tzinfo | None
134-
) -> _DatetimeT: ...
131+
def astimezone(self, tz: _tzinfo | None) -> Self: ... # type: ignore[override]
135132
def ctime(self) -> str: ...
136133
def isoformat(self, sep: str = ..., timespec: str = ...) -> str: ...
137134
@classmethod
@@ -147,16 +144,12 @@ class Timestamp(datetime):
147144
@overload # type: ignore[override]
148145
def __add__(self, other: np.ndarray) -> np.ndarray: ...
149146
@overload
150-
def __add__(
151-
self: _DatetimeT, other: timedelta | np.timedelta64 | Tick
152-
) -> _DatetimeT: ...
153-
def __radd__(self: _DatetimeT, other: timedelta) -> _DatetimeT: ...
147+
def __add__(self, other: timedelta | np.timedelta64 | Tick) -> Self: ...
148+
def __radd__(self, other: timedelta) -> Self: ...
154149
@overload # type: ignore[override]
155150
def __sub__(self, other: datetime) -> Timedelta: ...
156151
@overload
157-
def __sub__(
158-
self: _DatetimeT, other: timedelta | np.timedelta64 | Tick
159-
) -> _DatetimeT: ...
152+
def __sub__(self, other: timedelta | np.timedelta64 | Tick) -> Self: ...
160153
def __hash__(self) -> int: ...
161154
def weekday(self) -> int: ...
162155
def isoweekday(self) -> int: ...
@@ -181,25 +174,25 @@ class Timestamp(datetime):
181174
def to_julian_date(self) -> np.float64: ...
182175
@property
183176
def asm8(self) -> np.datetime64: ...
184-
def tz_convert(self: _DatetimeT, tz: _tzinfo | str | None) -> _DatetimeT: ...
177+
def tz_convert(self, tz: _tzinfo | str | None) -> Self: ...
185178
# TODO: could return NaT?
186179
def tz_localize(
187-
self: _DatetimeT,
180+
self,
188181
tz: _tzinfo | str | None,
189182
ambiguous: str = ...,
190183
nonexistent: str = ...,
191-
) -> _DatetimeT: ...
192-
def normalize(self: _DatetimeT) -> _DatetimeT: ...
184+
) -> Self: ...
185+
def normalize(self) -> Self: ...
193186
# TODO: round/floor/ceil could return NaT?
194187
def round(
195-
self: _DatetimeT, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
196-
) -> _DatetimeT: ...
188+
self, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
189+
) -> Self: ...
197190
def floor(
198-
self: _DatetimeT, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
199-
) -> _DatetimeT: ...
191+
self, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
192+
) -> Self: ...
200193
def ceil(
201-
self: _DatetimeT, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
202-
) -> _DatetimeT: ...
194+
self, freq: str, ambiguous: bool | str = ..., nonexistent: str = ...
195+
) -> Self: ...
203196
def day_name(self, locale: str | None = ...) -> str: ...
204197
def month_name(self, locale: str | None = ...) -> str: ...
205198
@property

pandas/_typing.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
tzinfo,
77
)
88
from os import PathLike
9+
import sys
910
from typing import (
1011
TYPE_CHECKING,
1112
Any,
@@ -83,8 +84,13 @@
8384
# Name "npt._ArrayLikeInt_co" is not defined [name-defined]
8485
NumpySorter = Optional[npt._ArrayLikeInt_co] # type: ignore[name-defined]
8586

87+
if sys.version_info >= (3, 11):
88+
from typing import Self
89+
else:
90+
from typing_extensions import Self # pyright: reportUnusedImport = false
8691
else:
8792
npt: Any = None
93+
Self: Any = None
8894

8995
HashableT = TypeVar("HashableT", bound=Hashable)
9096

pandas/core/arrays/datetimelike.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,6 +1851,8 @@ def __init__(
18511851
values = values.copy()
18521852
if freq:
18531853
freq = to_offset(freq)
1854+
if values.dtype.kind == "m" and not isinstance(freq, Tick):
1855+
raise TypeError("TimedeltaArray/Index freq must be a Tick")
18541856

18551857
NDArrayBacked.__init__(self, values=values, dtype=dtype)
18561858
self._freq = freq
@@ -1874,6 +1876,8 @@ def freq(self, value) -> None:
18741876
if value is not None:
18751877
value = to_offset(value)
18761878
self._validate_frequency(self, value)
1879+
if self.dtype.kind == "m" and not isinstance(value, Tick):
1880+
raise TypeError("TimedeltaArray/Index freq must be a Tick")
18771881

18781882
if self.ndim > 1:
18791883
raise ValueError("Cannot set freq with ndim > 1")
@@ -2067,9 +2071,9 @@ def _with_freq(self, freq):
20672071
# Always valid
20682072
pass
20692073
elif len(self) == 0 and isinstance(freq, BaseOffset):
2070-
# Always valid. In the TimedeltaArray case, we assume this
2071-
# is a Tick offset.
2072-
pass
2074+
# Always valid. In the TimedeltaArray case, we require a Tick offset
2075+
if self.dtype.kind == "m" and not isinstance(freq, Tick):
2076+
raise TypeError("TimedeltaArray/Index freq must be a Tick")
20732077
else:
20742078
# As an internal method, we can ensure this assertion always holds
20752079
assert freq == "infer"

pandas/core/arrays/timedeltas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ def _simple_new( # type: ignore[override]
202202
assert not tslibs.is_unitless(dtype)
203203
assert isinstance(values, np.ndarray), type(values)
204204
assert dtype == values.dtype
205+
assert freq is None or isinstance(freq, Tick)
205206

206207
result = super()._simple_new(values=values, dtype=dtype)
207208
result._freq = freq

pandas/core/resample.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,13 @@ def _get_time_delta_bins(self, ax: TimedeltaIndex):
18261826
f"an instance of {type(ax).__name__}"
18271827
)
18281828

1829+
if not isinstance(self.freq, Tick):
1830+
# GH#51896
1831+
raise ValueError(
1832+
"Resampling on a TimedeltaIndex requires fixed-duration `freq`, "
1833+
f"e.g. '24H' or '3D', not {self.freq}"
1834+
)
1835+
18291836
if not len(ax):
18301837
binner = labels = TimedeltaIndex(data=[], freq=self.freq, name=ax.name)
18311838
return binner, [], labels

pandas/core/series.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,10 @@ def view(self, dtype: Dtype | None = None) -> Series:
839839
# implementation
840840
res_values = self.array.view(dtype)
841841
res_ser = self._constructor(res_values, index=self.index)
842+
if isinstance(res_ser._mgr, SingleBlockManager) and using_copy_on_write():
843+
blk = res_ser._mgr._block
844+
blk.refs = cast("BlockValuesRefs", self._references)
845+
blk.refs.add_reference(blk) # type: ignore[arg-type]
842846
return res_ser.__finalize__(self, method="view")
843847

844848
# ----------------------------------------------------------------------

pandas/tests/copy_view/test_methods.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,3 +1672,21 @@ def test_transpose_ea_single_column(using_copy_on_write):
16721672
result = df.T
16731673

16741674
assert not np.shares_memory(get_array(df, "a"), get_array(result, 0))
1675+
1676+
1677+
def test_series_view(using_copy_on_write):
1678+
ser = Series([1, 2, 3])
1679+
ser_orig = ser.copy()
1680+
1681+
ser2 = ser.view()
1682+
assert np.shares_memory(get_array(ser), get_array(ser2))
1683+
if using_copy_on_write:
1684+
assert not ser2._mgr._has_no_reference(0)
1685+
1686+
ser2.iloc[0] = 100
1687+
1688+
if using_copy_on_write:
1689+
tm.assert_series_equal(ser_orig, ser)
1690+
else:
1691+
expected = Series([100, 2, 3])
1692+
tm.assert_series_equal(ser, expected)

0 commit comments

Comments
 (0)