From 52384d3425a32076e47ee32039b61f2d5a14b160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20W=C3=B6rtwein?= Date: Tue, 26 Dec 2023 12:19:10 -0500 Subject: [PATCH] Timestamp -> datetime and Timedelta -> timedelta (#841) * Timestamp -> datetime and Timedelta -> timedelta * keep redundancy * test * redundancy --- pandas-stubs/_libs/tslibs/timedeltas.pyi | 2 +- pandas-stubs/core/frame.pyi | 21 +++++++-------- pandas-stubs/core/indexes/accessors.pyi | 9 ++++++- pandas-stubs/core/indexes/datetimes.pyi | 13 ++++++--- pandas-stubs/core/indexes/timedeltas.pyi | 6 ++--- pandas-stubs/core/reshape/merge.pyi | 5 ++-- pandas-stubs/core/series.pyi | 23 ++++++++++------ pandas-stubs/tseries/holiday.pyi | 6 ++--- tests/test_frame.py | 1 + tests/test_indexes.py | 12 +++++++++ tests/test_pandas.py | 9 +++++++ tests/test_series.py | 25 +++++++++++++++++ tests/test_timefuncs.py | 34 +++++++++++++++++++++--- 13 files changed, 130 insertions(+), 36 deletions(-) diff --git a/pandas-stubs/_libs/tslibs/timedeltas.pyi b/pandas-stubs/_libs/tslibs/timedeltas.pyi index 9a71cfc07..51dedb581 100644 --- a/pandas-stubs/_libs/tslibs/timedeltas.pyi +++ b/pandas-stubs/_libs/tslibs/timedeltas.pyi @@ -210,7 +210,7 @@ class Timedelta(timedelta): @overload def __rsub__(self, other: timedelta | Timedelta | np.timedelta64) -> Timedelta: ... @overload - def __rsub__(self, other: Timestamp | np.datetime64) -> Timestamp: ... + def __rsub__(self, other: dt.datetime | Timestamp | np.datetime64) -> Timestamp: ... # type: ignore[misc] @overload def __rsub__(self, other: NaTType) -> NaTType: ... @overload diff --git a/pandas-stubs/core/frame.pyi b/pandas-stubs/core/frame.pyi index f413e6110..759c06dec 100644 --- a/pandas-stubs/core/frame.pyi +++ b/pandas-stubs/core/frame.pyi @@ -7,8 +7,7 @@ from collections.abc import ( MutableMapping, Sequence, ) -import datetime -import datetime as _dt +import datetime as dt from re import Pattern from typing import ( Any, @@ -373,7 +372,7 @@ class DataFrame(NDFrame, OpsMixin): convert_dates: dict[HashableT1, StataDateFormat] | None = ..., write_index: _bool = ..., byteorder: Literal["<", ">", "little", "big"] | None = ..., - time_stamp: _dt.datetime | None = ..., + time_stamp: dt.datetime | None = ..., data_label: _str | None = ..., variable_labels: dict[HashableT2, str] | None = ..., version: Literal[114, 117, 118, 119] | None = ..., @@ -1565,14 +1564,14 @@ class DataFrame(NDFrame, OpsMixin): ) -> DataFrame: ... def at_time( self, - time: _str | datetime.time, + time: _str | dt.time, asof: _bool = ..., axis: Axis | None = ..., ) -> DataFrame: ... def between_time( self, - start_time: _str | datetime.time, - end_time: _str | datetime.time, + start_time: _str | dt.time, + end_time: _str | dt.time, axis: Axis | None = ..., ) -> DataFrame: ... @overload @@ -1941,7 +1940,7 @@ class DataFrame(NDFrame, OpsMixin): level: Level | None = ..., origin: Timestamp | Literal["epoch", "start", "start_day", "end", "end_day"] = ..., - offset: Timedelta | _str | None = ..., + offset: dt.timedelta | Timedelta | _str | None = ..., group_keys: _bool = ..., ) -> Resampler[DataFrame]: ... def rfloordiv( @@ -1968,7 +1967,7 @@ class DataFrame(NDFrame, OpsMixin): @overload def rolling( self, - window: int | str | _dt.timedelta | BaseOffset | BaseIndexer, + window: int | str | dt.timedelta | BaseOffset | BaseIndexer, min_periods: int | None = ..., center: _bool = ..., on: Hashable | None = ..., @@ -1982,7 +1981,7 @@ class DataFrame(NDFrame, OpsMixin): @overload def rolling( self, - window: int | str | _dt.timedelta | BaseOffset | BaseIndexer, + window: int | str | dt.timedelta | BaseOffset | BaseIndexer, min_periods: int | None = ..., center: _bool = ..., on: Hashable | None = ..., @@ -2217,8 +2216,8 @@ class DataFrame(NDFrame, OpsMixin): ) -> DataFrame: ... def truncate( self, - before: datetime.date | _str | int | None = ..., - after: datetime.date | _str | int | None = ..., + before: dt.date | _str | int | None = ..., + after: dt.date | _str | int | None = ..., axis: Axis | None = ..., copy: _bool = ..., ) -> DataFrame: ... diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index 38284618d..1accd842f 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -1,5 +1,8 @@ import datetime as dt -from datetime import tzinfo +from datetime import ( + timedelta, + tzinfo, +) from typing import ( Generic, Literal, @@ -166,6 +169,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]): freq: str | BaseOffset | None, ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ..., nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"] + | timedelta | Timedelta = ..., ) -> _DTRoundingMethodReturnType: ... def floor( @@ -173,6 +177,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]): freq: str | BaseOffset | None, ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ..., nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"] + | timedelta | Timedelta = ..., ) -> _DTRoundingMethodReturnType: ... def ceil( @@ -180,6 +185,7 @@ class _DatetimeRoundingMethods(Generic[_DTRoundingMethodReturnType]): freq: str | BaseOffset | None, ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ..., nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"] + | timedelta | Timedelta = ..., ) -> _DTRoundingMethodReturnType: ... @@ -206,6 +212,7 @@ class _DatetimeLikeNoTZMethods( tz: tzinfo | str | None, ambiguous: Literal["raise", "infer", "NaT"] | np_ndarray_bool = ..., nonexistent: Literal["shift_forward", "shift_backward", "NaT", "raise"] + | timedelta | Timedelta = ..., ) -> _DTNormalizeReturnType: ... def tz_convert(self, tz: tzinfo | str | None) -> _DTNormalizeReturnType: ... diff --git a/pandas-stubs/core/indexes/datetimes.pyi b/pandas-stubs/core/indexes/datetimes.pyi index 43ee557c7..57ae07935 100644 --- a/pandas-stubs/core/indexes/datetimes.pyi +++ b/pandas-stubs/core/indexes/datetimes.pyi @@ -3,6 +3,7 @@ from collections.abc import ( Sequence, ) from datetime import ( + datetime, timedelta, tzinfo, ) @@ -59,13 +60,19 @@ class DatetimeIndex(DatetimeTimedeltaMixin[Timestamp], DatetimeIndexProperties): @overload def __add__(self, other: TimedeltaSeries) -> TimestampSeries: ... @overload - def __add__(self, other: Timedelta | TimedeltaIndex) -> DatetimeIndex: ... + def __add__( + self, other: timedelta | Timedelta | TimedeltaIndex + ) -> DatetimeIndex: ... @overload def __sub__(self, other: TimedeltaSeries) -> TimestampSeries: ... @overload - def __sub__(self, other: Timedelta | TimedeltaIndex) -> DatetimeIndex: ... + def __sub__( + self, other: timedelta | Timedelta | TimedeltaIndex + ) -> DatetimeIndex: ... @overload - def __sub__(self, other: Timestamp | DatetimeIndex) -> TimedeltaIndex: ... + def __sub__( + self, other: datetime | Timestamp | DatetimeIndex + ) -> TimedeltaIndex: ... def to_series(self, index=..., name=...) -> TimestampSeries: ... def snap(self, freq: str = ...): ... def get_value(self, series, key): ... diff --git a/pandas-stubs/core/indexes/timedeltas.pyi b/pandas-stubs/core/indexes/timedeltas.pyi index e97824395..2f44f851e 100644 --- a/pandas-stubs/core/indexes/timedeltas.pyi +++ b/pandas-stubs/core/indexes/timedeltas.pyi @@ -52,9 +52,9 @@ class TimedeltaIndex(DatetimeTimedeltaMixin[Timedelta], TimedeltaIndexProperties @overload def __add__(self, other: DatetimeIndex) -> DatetimeIndex: ... @overload - def __add__(self, other: Timedelta | Self) -> Self: ... - def __radd__(self, other: Timestamp | DatetimeIndex) -> DatetimeIndex: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - def __sub__(self, other: Timedelta | Self) -> Self: ... + def __add__(self, other: dt.timedelta | Timedelta | Self) -> Self: ... + def __radd__(self, other: dt.datetime | Timestamp | DatetimeIndex) -> DatetimeIndex: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + def __sub__(self, other: dt.timedelta | Timedelta | Self) -> Self: ... def __mul__(self, other: num) -> Self: ... @overload # type: ignore[override] def __truediv__(self, other: num | Sequence[float]) -> Self: ... diff --git a/pandas-stubs/core/reshape/merge.pyi b/pandas-stubs/core/reshape/merge.pyi index 71d8651db..d80d3ef7a 100644 --- a/pandas-stubs/core/reshape/merge.pyi +++ b/pandas-stubs/core/reshape/merge.pyi @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import ( Literal, overload, @@ -6,9 +7,9 @@ from typing import ( from pandas import ( DataFrame, Series, + Timedelta, ) -from pandas._libs.tslibs import Timedelta from pandas._typing import ( AnyArrayLike, HashableT, @@ -99,7 +100,7 @@ def merge_asof( | tuple[str, str] | tuple[None, str] | tuple[str, None] = ..., - tolerance: int | Timedelta | None = ..., + tolerance: int | timedelta | Timedelta | None = ..., allow_exact_matches: bool = ..., direction: Literal["backward", "forward", "nearest"] = ..., ) -> DataFrame: ... diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 2a42e8a3d..407389794 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -1352,9 +1352,10 @@ class Series(IndexOpsMixin[S1], NDFrame): base: int = ..., on: _str | None = ..., level: Level | None = ..., - origin: Timestamp + origin: datetime + | Timestamp | Literal["epoch", "start", "start_day", "end", "end_day"] = ..., - offset: Timedelta | _str | None = ..., + offset: timedelta | Timedelta | _str | None = ..., ) -> Resampler[Series]: ... def first(self, offset) -> Series[S1]: ... def last(self, offset) -> Series[S1]: ... @@ -1456,7 +1457,8 @@ class Series(IndexOpsMixin[S1], NDFrame): def __add__(self, other: S1 | Self) -> Self: ... @overload def __add__( - self, other: num | _str | Timedelta | _ListLike | Series | np.timedelta64 + self, + other: num | _str | timedelta | Timedelta | _ListLike | Series | np.timedelta64, ) -> Series: ... # ignore needed for mypy as we want different results based on the arguments @overload # type: ignore[override] @@ -1485,7 +1487,7 @@ class Series(IndexOpsMixin[S1], NDFrame): ) -> Series[_bool]: ... @overload def __mul__( - self, other: Timedelta | TimedeltaSeries | np.timedelta64 + self, other: timedelta | Timedelta | TimedeltaSeries | np.timedelta64 ) -> TimedeltaSeries: ... @overload def __mul__(self, other: num | _ListLike | Series) -> Series: ... @@ -2043,18 +2045,23 @@ class TimedeltaSeries(Series[Timedelta]): def __add__(self, other: Period) -> PeriodSeries: ... @overload def __add__( - self, other: Timestamp | TimestampSeries | DatetimeIndex + self, other: datetime | Timestamp | TimestampSeries | DatetimeIndex ) -> TimestampSeries: ... @overload def __add__( # pyright: ignore[reportIncompatibleMethodOverride] - self, other: Timedelta | np.timedelta64 + self, other: timedelta | Timedelta | np.timedelta64 ) -> TimedeltaSeries: ... - def __radd__(self, other: Timestamp | TimestampSeries) -> TimestampSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + def __radd__(self, other: datetime | Timestamp | TimestampSeries) -> TimestampSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] def __mul__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] self, other: num | Sequence[num] | Series[int] | Series[float] ) -> TimedeltaSeries: ... def __sub__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - self, other: Timedelta | TimedeltaSeries | TimedeltaIndex | np.timedelta64 + self, + other: timedelta + | Timedelta + | TimedeltaSeries + | TimedeltaIndex + | np.timedelta64, ) -> TimedeltaSeries: ... @overload # type: ignore[override] def __truediv__(self, other: float | Sequence[float]) -> Self: ... diff --git a/pandas-stubs/tseries/holiday.pyi b/pandas-stubs/tseries/holiday.pyi index 4716fc666..84e63067c 100644 --- a/pandas-stubs/tseries/holiday.pyi +++ b/pandas-stubs/tseries/holiday.pyi @@ -71,9 +71,9 @@ def register(cls: type[AbstractHolidayCalendar]) -> None: ... def get_calendar(name: str) -> AbstractHolidayCalendar: ... class AbstractHolidayCalendar: - rules: list[Holiday] = ... - start_date: Timestamp = ... - end_date: Timestamp = ... + rules: list[Holiday] + start_date: Timestamp + end_date: Timestamp def __init__(self, name: str = "", rules: list[Holiday] | None = None) -> None: ... def rule_from_name(self, name: str) -> Holiday | None: ... diff --git a/tests/test_frame.py b/tests/test_frame.py index e3875432f..8eb232857 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -1359,6 +1359,7 @@ def test_types_resample() -> None: with pytest_warns_bounded(FutureWarning, "'M' is deprecated", lower="2.1.99"): df.resample("M", on="date") df.resample("20min", origin="epoch", offset=pd.Timedelta(2, "minutes"), on="date") + df.resample("20min", origin="epoch", offset=datetime.timedelta(2), on="date") def test_types_to_dict() -> None: diff --git a/tests/test_indexes.py b/tests/test_indexes.py index f878ba17b..24d21a091 100644 --- a/tests/test_indexes.py +++ b/tests/test_indexes.py @@ -1048,3 +1048,15 @@ def test_timedelta_div() -> None: [1] / index # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues] 1 // index # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues] [1] // index # type: ignore[operator] # pyright: ignore[reportGeneralTypeIssues] + + +def test_datetime_operators_builtin() -> None: + time = pd.date_range("2022-01-01", "2022-01-31", freq="D") + check(assert_type(time + dt.timedelta(0), pd.DatetimeIndex), pd.DatetimeIndex) + check(assert_type(time - dt.timedelta(0), pd.DatetimeIndex), pd.DatetimeIndex) + check(assert_type(time - dt.datetime.now(), pd.TimedeltaIndex), pd.TimedeltaIndex) + + delta = check(assert_type(time - time, pd.TimedeltaIndex), pd.TimedeltaIndex) + check(assert_type(delta + dt.timedelta(0), pd.TimedeltaIndex), pd.TimedeltaIndex) + check(assert_type(dt.datetime.now() + delta, pd.DatetimeIndex), pd.DatetimeIndex) + check(assert_type(delta - dt.timedelta(0), pd.TimedeltaIndex), pd.TimedeltaIndex) diff --git a/tests/test_pandas.py b/tests/test_pandas.py index 2ecb7f392..c94257461 100644 --- a/tests/test_pandas.py +++ b/tests/test_pandas.py @@ -1412,6 +1412,15 @@ def test_merge_asof() -> None: ), pd.DataFrame, ) + check( + assert_type( + pd.merge_asof( + trades, quotes, on="time", by="ticker", tolerance=dt.timedelta(1) + ), + pd.DataFrame, + ), + pd.DataFrame, + ) check( assert_type( pd.merge_asof( diff --git a/tests/test_series.py b/tests/test_series.py index ba6ae57f7..cf3762214 100644 --- a/tests/test_series.py +++ b/tests/test_series.py @@ -994,6 +994,7 @@ def test_types_resample() -> None: s.resample("3min").sum() # origin and offset params added in 1.1.0 https://pandas.pydata.org/docs/whatsnew/v1.1.0.html s.resample("20min", origin="epoch", offset=pd.Timedelta(value=2, unit="minutes")) + s.resample("20min", origin=datetime.datetime.now(), offset=datetime.timedelta(1)) # set_flags() method added in 1.2.0 https://pandas.pydata.org/docs/whatsnew/v1.2.0.html @@ -2860,3 +2861,27 @@ def test_round() -> None: def test_series_new_empty() -> None: # GH 826 check(assert_type(pd.Series(), "pd.Series[Any]"), pd.Series) + + +def test_timedeltaseries_operators() -> None: + series = pd.Series([pd.Timedelta(days=1)]) + check( + assert_type(series + datetime.datetime.now(), TimestampSeries), + pd.Series, + pd.Timestamp, + ) + check( + assert_type(series + datetime.timedelta(1), TimedeltaSeries), + pd.Series, + pd.Timedelta, + ) + check( + assert_type(datetime.datetime.now() + series, TimestampSeries), + pd.Series, + pd.Timestamp, + ) + check( + assert_type(series - datetime.timedelta(1), TimedeltaSeries), + pd.Series, + pd.Timedelta, + ) diff --git a/tests/test_timefuncs.py b/tests/test_timefuncs.py index 3473f2897..2bfad7d1b 100644 --- a/tests/test_timefuncs.py +++ b/tests/test_timefuncs.py @@ -373,7 +373,10 @@ def test_series_dt_accessors() -> None: assert_type(s0.dt.tz_localize(None), "TimestampSeries"), pd.Series, pd.Timestamp ) check( - assert_type(s0.dt.tz_localize(pytz.UTC), "TimestampSeries"), + assert_type( + s0.dt.tz_localize(pytz.UTC, nonexistent=dt.timedelta(0)), + "TimestampSeries", + ), pd.Series, pd.Timestamp, ) @@ -408,9 +411,21 @@ def test_series_dt_accessors() -> None: check(assert_type(s0_local.dt.tz, Optional[dt.tzinfo]), dt.tzinfo) check(assert_type(s0.dt.normalize(), "TimestampSeries"), pd.Series, pd.Timestamp) check(assert_type(s0.dt.strftime("%Y"), "pd.Series[str]"), pd.Series, str) - check(assert_type(s0.dt.round("D"), "TimestampSeries"), pd.Series, pd.Timestamp) - check(assert_type(s0.dt.floor("D"), "TimestampSeries"), pd.Series, pd.Timestamp) - check(assert_type(s0.dt.ceil("D"), "TimestampSeries"), pd.Series, pd.Timestamp) + check( + assert_type(s0.dt.round("D", nonexistent=dt.timedelta(1)), "TimestampSeries"), + pd.Series, + pd.Timestamp, + ) + check( + assert_type(s0.dt.floor("D", nonexistent=dt.timedelta(1)), "TimestampSeries"), + pd.Series, + pd.Timestamp, + ) + check( + assert_type(s0.dt.ceil("D", nonexistent=dt.timedelta(1)), "TimestampSeries"), + pd.Series, + pd.Timestamp, + ) check(assert_type(s0.dt.month_name(), "pd.Series[str]"), pd.Series, str) check(assert_type(s0.dt.day_name(), "pd.Series[str]"), pd.Series, str) @@ -1234,3 +1249,14 @@ def test_date_range_unit(): ), pd.DatetimeIndex, ) + + +def test_DatetimeIndex_sub_timedelta() -> None: + # GH838 + check( + assert_type( + pd.date_range("2023-01-01", periods=10, freq="1D") - dt.timedelta(days=1), + "pd.DatetimeIndex", + ), + pd.DatetimeIndex, + )