Skip to content

Commit 78f835c

Browse files
mutricylLaurent Mutricy
and
Laurent Mutricy
authored
821 series diff timestamp2 (#907)
* Adding tests for #821 * finish to add tests from pd._types.S1 definition * start fixing stubs * date le lt ge gt fixing * new diff overloads * adding a few Never * fix: check function updated for series.diff * clean comments and update period test * cover windows specific RuntimeWarning * PeriodSeries.diff is OffsetSeries * remove comments * try tables 3.9.2 to fix macos github test --------- Co-authored-by: Laurent Mutricy <[email protected]>
1 parent cfb5773 commit 78f835c

File tree

4 files changed

+148
-12
lines changed

4 files changed

+148
-12
lines changed

pandas-stubs/core/series.pyi

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,11 @@ class Series(IndexOpsMixin[S1], NDFrame):
235235
cls,
236236
data: (
237237
DatetimeIndex
238-
| Sequence[np.datetime64 | datetime]
239-
| dict[HashableT1, np.datetime64 | datetime]
238+
| Sequence[np.datetime64 | datetime | date]
239+
| dict[HashableT1, np.datetime64 | datetime | date]
240240
| np.datetime64
241241
| datetime
242+
| date
242243
),
243244
index: Axes | None = ...,
244245
*,
@@ -259,7 +260,7 @@ class Series(IndexOpsMixin[S1], NDFrame):
259260
@overload
260261
def __new__( # type: ignore[overload-overlap]
261262
cls,
262-
data: PeriodIndex,
263+
data: PeriodIndex | Sequence[Period],
263264
index: Axes | None = ...,
264265
*,
265266
dtype: PeriodDtype = ...,
@@ -748,7 +749,18 @@ class Series(IndexOpsMixin[S1], NDFrame):
748749
def cov(
749750
self, other: Series[S1], min_periods: int | None = ..., ddof: int = ...
750751
) -> float: ...
751-
def diff(self, periods: int = ...) -> Series[S1]: ...
752+
@overload
753+
def diff(self: Series[_bool], periods: int = ...) -> Series[type[object]]: ... # type: ignore[overload-overlap]
754+
@overload
755+
def diff(self: Series[complex], periods: int = ...) -> Series[complex]: ... # type: ignore[overload-overlap]
756+
@overload
757+
def diff(self: Series[bytes], periods: int = ...) -> Never: ...
758+
@overload
759+
def diff(self: Series[type], periods: int = ...) -> Never: ...
760+
@overload
761+
def diff(self: Series[str], periods: int = ...) -> Never: ...
762+
@overload
763+
def diff(self, periods: int = ...) -> Series[float]: ...
752764
def autocorr(self, lag: int = ...) -> float: ...
753765
@overload
754766
def dot(self, other: Series[S1]) -> Scalar: ...
@@ -1511,16 +1523,16 @@ class Series(IndexOpsMixin[S1], NDFrame):
15111523
def __eq__(self, other: object) -> Series[_bool]: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
15121524
def __floordiv__(self, other: num | _ListLike | Series[S1]) -> Series[int]: ...
15131525
def __ge__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
1514-
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta
1526+
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta | date
15151527
) -> Series[_bool]: ...
15161528
def __gt__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
1517-
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta
1529+
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta | date
15181530
) -> Series[_bool]: ...
15191531
def __le__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
1520-
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta
1532+
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta | date
15211533
) -> Series[_bool]: ...
15221534
def __lt__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
1523-
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta
1535+
self, other: S1 | _ListLike | Series[S1] | datetime | timedelta | date
15241536
) -> Series[_bool]: ...
15251537
@overload
15261538
def __mul__(
@@ -2075,6 +2087,7 @@ class TimestampSeries(Series[Timestamp]):
20752087
numeric_only: _bool = ...,
20762088
**kwargs,
20772089
) -> Timedelta: ...
2090+
def diff(self, periods: int = ...) -> TimedeltaSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
20782091

20792092
class TimedeltaSeries(Series[Timedelta]):
20802093
# ignores needed because of mypy
@@ -2171,11 +2184,13 @@ class TimedeltaSeries(Series[Timedelta]):
21712184
numeric_only: _bool = ...,
21722185
**kwargs,
21732186
) -> Timedelta: ...
2187+
def diff(self, periods: int = ...) -> TimedeltaSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
21742188

21752189
class PeriodSeries(Series[Period]):
21762190
@property
21772191
def dt(self) -> PeriodProperties: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
21782192
def __sub__(self, other: PeriodSeries) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
2193+
def diff(self, periods: int = ...) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
21792194

21802195
class OffsetSeries(Series[BaseOffset]):
21812196
@overload # type: ignore[override]
@@ -2188,3 +2203,4 @@ class OffsetSeries(Series[BaseOffset]):
21882203
class IntervalSeries(Series[Interval[_OrderableT]], Generic[_OrderableT]):
21892204
@property
21902205
def array(self) -> IntervalArray: ...
2206+
def diff(self, periods: int = ...) -> Never: ...

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ black = ">=23.3.0"
4949
isort = ">=5.12.0"
5050
openpyxl = ">=3.0.10"
5151
# for tables, MacOS gives random CI failures on 3.9.2
52-
tables = { version = "==3.9.1", python = "<4"} # 3.8.0 depends on blosc2 which caps python to <4
52+
tables = { version = "==3.9.2", python = "<4"} # 3.8.0 depends on blosc2 which caps python to <4
5353
lxml = ">=4.9.1"
5454
pyreadstat = ">=1.2.0"
5555
xlrd = ">=2.0.1"

tests/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import (
1111
TYPE_CHECKING,
1212
Final,
13+
Literal,
1314
)
1415

1516
import pandas as pd
@@ -24,16 +25,22 @@
2425
PD_LTE_22 = Version(pd.__version__) < Version("2.2.999")
2526

2627

27-
def check(actual: T, klass: type, dtype: type | None = None, attr: str = "left") -> T:
28+
def check(
29+
actual: T,
30+
klass: type,
31+
dtype: type | None = None,
32+
attr: str = "left",
33+
index_to_check_for_type: Literal[0, -1] = 0,
34+
) -> T:
2835
if not isinstance(actual, klass):
2936
raise RuntimeError(f"Expected type '{klass}' but got '{type(actual)}'")
3037
if dtype is None:
3138
return actual # type: ignore[return-value]
3239

3340
if isinstance(actual, pd.Series):
34-
value = actual.iloc[0]
41+
value = actual.iloc[index_to_check_for_type]
3542
elif isinstance(actual, pd.Index):
36-
value = actual[0] # type: ignore[assignment]
43+
value = actual[index_to_check_for_type] # type: ignore[assignment]
3744
elif isinstance(actual, BaseGroupBy):
3845
value = actual.obj
3946
elif hasattr(actual, "__iter__"):

tests/test_series.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
from typing_extensions import (
3333
Self,
3434
TypeAlias,
35+
assert_never,
3536
assert_type,
3637
)
3738
import xarray as xr
3839

3940
from pandas._libs.missing import NAType
41+
from pandas._libs.tslibs import BaseOffset
4042
from pandas._typing import (
4143
DtypeObj,
4244
Scalar,
@@ -45,19 +47,22 @@
4547
from tests import (
4648
PD_LTE_22,
4749
TYPE_CHECKING_INVALID_USAGE,
50+
WINDOWS,
4851
check,
4952
pytest_warns_bounded,
5053
)
5154
from tests.extension.decimal.array import DecimalDtype
5255

5356
if TYPE_CHECKING:
5457
from pandas.core.series import (
58+
OffsetSeries,
5559
TimedeltaSeries,
5660
TimestampSeries,
5761
)
5862
else:
5963
TimedeltaSeries: TypeAlias = pd.Series
6064
TimestampSeries: TypeAlias = pd.Series
65+
OffsetSeries: TypeAlias = pd.Series
6166

6267
if TYPE_CHECKING:
6368
from pandas._typing import (
@@ -3091,3 +3096,111 @@ def test_series_apply() -> None:
30913096
check(assert_type(s.apply(list), "pd.Series[Any]"), pd.Series)
30923097
check(assert_type(s.apply(set), "pd.Series[Any]"), pd.Series)
30933098
check(assert_type(s.apply(frozenset), "pd.Series[Any]"), pd.Series)
3099+
3100+
3101+
def test_diff() -> None:
3102+
s = pd.Series([1, 1, 2, 3, 5, 8])
3103+
# int -> float
3104+
check(assert_type(s.diff(), "pd.Series[float]"), pd.Series, float)
3105+
# unint -> float
3106+
check(assert_type(s.astype(np.uint32).diff(), "pd.Series[float]"), pd.Series, float)
3107+
# float -> float
3108+
check(assert_type(s.astype(float).diff(), "pd.Series[float]"), pd.Series, float)
3109+
# datetime.date -> timeDelta
3110+
check(
3111+
assert_type(
3112+
pd.Series(
3113+
[datetime.datetime.now().date(), datetime.datetime.now().date()]
3114+
).diff(),
3115+
"TimedeltaSeries",
3116+
),
3117+
pd.Series,
3118+
pd.Timedelta,
3119+
index_to_check_for_type=-1,
3120+
)
3121+
# timestamp -> timedelta
3122+
times = pd.Series([pd.Timestamp(0), pd.Timestamp(1)])
3123+
check(
3124+
assert_type(times.diff(), "TimedeltaSeries"),
3125+
pd.Series,
3126+
pd.Timedelta,
3127+
index_to_check_for_type=-1,
3128+
)
3129+
# timedelta -> timedelta64
3130+
check(
3131+
assert_type(
3132+
pd.Series([pd.Timedelta(0), pd.Timedelta(1)]).diff(), "TimedeltaSeries"
3133+
),
3134+
pd.Series,
3135+
pd.Timedelta,
3136+
index_to_check_for_type=-1,
3137+
)
3138+
# period -> object
3139+
if WINDOWS:
3140+
with pytest_warns_bounded(
3141+
RuntimeWarning, "overflow encountered in scalar multiply"
3142+
):
3143+
check(
3144+
assert_type(
3145+
pd.Series(
3146+
pd.period_range(start="2017-01-01", end="2017-02-01", freq="D")
3147+
).diff(),
3148+
"OffsetSeries",
3149+
),
3150+
pd.Series,
3151+
BaseOffset,
3152+
index_to_check_for_type=-1,
3153+
)
3154+
else:
3155+
check(
3156+
assert_type(
3157+
pd.Series(
3158+
pd.period_range(start="2017-01-01", end="2017-02-01", freq="D")
3159+
).diff(),
3160+
"OffsetSeries",
3161+
),
3162+
pd.Series,
3163+
BaseOffset,
3164+
index_to_check_for_type=-1,
3165+
)
3166+
# bool -> object
3167+
check(
3168+
assert_type(
3169+
pd.Series([True, True, False, False, True]).diff(),
3170+
"pd.Series[type[object]]",
3171+
),
3172+
pd.Series,
3173+
object,
3174+
)
3175+
# object -> object
3176+
check(
3177+
assert_type(s.astype(object).diff(), "pd.Series[type[object]]"),
3178+
pd.Series,
3179+
object,
3180+
)
3181+
# complex -> complex
3182+
check(
3183+
assert_type(s.astype(complex).diff(), "pd.Series[complex]"), pd.Series, complex
3184+
)
3185+
if TYPE_CHECKING_INVALID_USAGE:
3186+
# interval -> TypeError: IntervalArray has no 'diff' method. Convert to a suitable dtype prior to calling 'diff'.
3187+
assert_never(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff())
3188+
3189+
3190+
def test_diff_never1() -> None:
3191+
s = pd.Series([1, 1, 2, 3, 5, 8])
3192+
if TYPE_CHECKING_INVALID_USAGE:
3193+
# bytes -> numpy.core._exceptions._UFuncNoLoopError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('S21'), dtype('S21')) -> None
3194+
assert_never(s.astype(bytes).diff())
3195+
3196+
3197+
def test_diff_never2() -> None:
3198+
if TYPE_CHECKING_INVALID_USAGE:
3199+
# dtype -> TypeError: unsupported operand type(s) for -: 'type' and 'type'
3200+
assert_never(pd.Series([str, int, bool]).diff())
3201+
3202+
3203+
def test_diff_never3() -> None:
3204+
if TYPE_CHECKING_INVALID_USAGE:
3205+
# str -> TypeError: unsupported operand type(s) for -: 'str' and 'str'
3206+
assert_never(pd.Series(["a", "b"]).diff())

0 commit comments

Comments
 (0)