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
35 changes: 9 additions & 26 deletions pandas-stubs/core/series.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ from pandas.plotting import PlotAccessor
class _SupportsAdd(Protocol[_T_co]):
def __add__(self, value: Self, /) -> _T_co: ...

@type_check_only
class _SupportsMul(Protocol[_T_co]):
def __mul__(self, value: Self, /) -> _T_co: ...

class _iLocIndexerSeries(_iLocIndexer, Generic[S1]):
# get item
@overload
Expand Down Expand Up @@ -3821,23 +3825,23 @@ class Series(IndexOpsMixin[S1], NDFrame):
) -> Series[S1]: ...
@overload
def cumprod(
self: Series[_str],
self: Series[Never],
axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
**kwargs: Any,
) -> Never: ...
) -> Series: ...
@overload
def cumprod(
self: Series[Timestamp],
self: Series[bool],
axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
**kwargs: Any,
) -> Never: ...
) -> Series[int]: ...
@overload
def cumprod(
self,
self: SupportsGetItem[Scalar, _SupportsMul[S1]],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that work with classes that have overloads for __mul__? My understanding from microsoft/pyright#6549 (comment) is that type checkers will then pick the first overload. It might be okay in this case (and I wish we could use this pattern!) - just something to be aware of when using this patterns in more places.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that it would matter, because S1 are types we define in pandas as well as fundamental types (int, float, etc.), so I think we will be fine. It's a bit different than the case you wrote up, because of the use of _SupportsMul[S1] requiring the type that supports __mul__() to be in S1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I with this pattern can help us saving efforts, too.

  • Note that there are already SupportsAdd and SupportsMul in _typeshed. They are however different somehow from our _SupportsAdd and _SupportsMul. We probably need a renaming in the near future.
  • It would be great if we could work out a pattern that deal with typing changes like A.__operate__(B) ->B and A.__operate__(A) -> B, for example bool + int -> int, int / int -> float.

axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
Expand Down Expand Up @@ -4369,39 +4373,18 @@ class TimedeltaSeries(_SeriesSubclassBase[Timedelta, np.timedelta64]):
*args: Any,
**kwargs: Any,
) -> TimedeltaSeries: ...
def cumprod( # pyrefly: ignore
self,
axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
**kwargs: Any,
) -> Never: ...

class PeriodSeries(_SeriesSubclassBase[Period, np.object_]):
@property
def dt(self) -> PeriodProperties: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
def __sub__(self, other: PeriodSeries) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
def diff(self, periods: int = ...) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
def cumprod(
self,
axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
**kwargs: Any,
) -> Never: ...

class OffsetSeries(_SeriesSubclassBase[BaseOffset, np.object_]):
@overload # type: ignore[override]
def __radd__(self, other: Period) -> PeriodSeries: ...
@overload
def __radd__(self, other: BaseOffset) -> OffsetSeries: ...
def cumprod( # pyrefly: ignore
self,
axis: AxisIndex = ...,
skipna: _bool = ...,
*args: Any,
**kwargs: Any,
) -> Never: ...

class IntervalSeries(
_SeriesSubclassBase[Interval[_OrderableT], np.object_], Generic[_OrderableT]
Expand Down
51 changes: 51 additions & 0 deletions tests/series/test_cumul.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import numpy as np
import pandas as pd
from typing_extensions import assert_type

from tests import (
TYPE_CHECKING_INVALID_USAGE,
check,
)


def test_cumul_any_float() -> None:
series = pd.DataFrame({"A": [1.0, float("nan"), 2.0]})["A"]
check(assert_type(series.cumprod(), pd.Series), pd.Series, np.floating)


def test_cumul_bool() -> None:
series = pd.Series([True, False, True])
check(assert_type(series.cumprod(), "pd.Series[int]"), pd.Series, np.integer)


def test_cumul_int() -> None:
series = pd.Series([3, 1, 2])
check(assert_type(series.cumprod(), "pd.Series[int]"), pd.Series, np.integer)


def test_cumul_float() -> None:
series = pd.Series([3.0, float("nan"), 2.0])
check(assert_type(series.cumprod(), "pd.Series[float]"), pd.Series, np.floating)


def test_cumul_complex() -> None:
series = pd.Series([3j, 3 + 4j, 2j])
check(
assert_type(series.cumprod(), "pd.Series[complex]"),
pd.Series,
np.complexfloating,
)


def test_cumul_str() -> None:
series = pd.Series(["1", "a", "🐼"])
if TYPE_CHECKING_INVALID_USAGE:
series.cumprod() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue]


def test_cumul_ts() -> None:
series = pd.Series(pd.to_datetime(["2025-09-18", "2025-09-18", "2025-09-18"]))
check(assert_type(series, "pd.Series[pd.Timestamp]"), pd.Series, pd.Timestamp)

if TYPE_CHECKING_INVALID_USAGE:
series.cumprod() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue]
18 changes: 6 additions & 12 deletions tests/series/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -3946,24 +3946,18 @@ def test_timedelta_index_cumprod() -> None:
offset_series = as_period_series - as_period_series

if TYPE_CHECKING_INVALID_USAGE:
assert_type(pd.Series(["a", "b"]).cumprod(), Never)
offset_series.cumprod() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue]

if TYPE_CHECKING_INVALID_USAGE:
assert_type(offset_series.cumprod(), Never)
pd.Series([pd.Timedelta(0), pd.Timedelta(1)]).cumprod() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue]

if TYPE_CHECKING_INVALID_USAGE:
assert_type(pd.Series([pd.Timedelta(0), pd.Timedelta(1)]).cumprod(), Never)
pd.Series( # type: ignore[misc]
[pd.Timestamp("2024-04-29"), pd.Timestamp("2034-08-28")]
).cumprod() # pyright: ignore[reportAttributeAccessIssue]

if TYPE_CHECKING_INVALID_USAGE:
assert_type(
pd.Series(
[pd.Timestamp("2024-04-29"), pd.Timestamp("2034-08-28")]
).cumprod(),
Never,
)

if TYPE_CHECKING_INVALID_USAGE:
assert_type(as_period_series.cumprod(), Never)
as_period_series.cumprod() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue]


def test_series_str_methods() -> None:
Expand Down
Loading