From 7c4429516466bf1097b39cb2c8d10aa0669b1763 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sun, 28 Sep 2025 01:26:26 +0200 Subject: [PATCH 01/14] refactor: drop IntervalSeries --- pandas-stubs/core/base.pyi | 5 +- pandas-stubs/core/indexes/accessors.pyi | 95 ++++++++++++------------- pandas-stubs/core/indexes/base.pyi | 5 +- pandas-stubs/core/indexes/datetimes.pyi | 5 -- pandas-stubs/core/indexes/period.pyi | 2 + pandas-stubs/core/series.pyi | 54 +++++++------- tests/indexes/test_indexes.py | 80 +++++++++++++++++++-- tests/series/test_properties.py | 62 ++++++++++++++++ tests/test_extension.py | 2 +- tests/test_timefuncs.py | 9 +-- 10 files changed, 218 insertions(+), 101 deletions(-) create mode 100644 tests/series/test_properties.py diff --git a/pandas-stubs/core/base.pyi b/pandas-stubs/core/base.pyi index c116a717e..3aa09dc57 100644 --- a/pandas-stubs/core/base.pyi +++ b/pandas-stubs/core/base.pyi @@ -5,6 +5,7 @@ from collections.abc import ( ) from typing import ( Any, + ClassVar, Generic, Literal, TypeAlias, @@ -20,6 +21,7 @@ from pandas import ( from pandas.core.arraylike import OpsMixin from pandas.core.arrays import ExtensionArray from pandas.core.arrays.categorical import Categorical +from pandas.core.indexes.accessors import ArrayDescriptor from typing_extensions import Self from pandas._typing import ( @@ -69,8 +71,7 @@ class IndexOpsMixin(OpsMixin, Generic[S1, GenericT_co]): def nbytes(self) -> int: ... @property def size(self) -> int: ... - @property - def array(self) -> ExtensionArray: ... + array: ClassVar = ArrayDescriptor() # noqa @overload def to_numpy( self, diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index 175756f39..4414f706c 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -1,14 +1,15 @@ -import datetime as dt from datetime import ( + date, + time, timedelta, tzinfo as _tzinfo, ) from typing import ( - Any, Generic, Literal, TypeVar, overload, + type_check_only, ) import numpy as np @@ -25,18 +26,24 @@ from pandas.core.arrays import ( DatetimeArray, PeriodArray, ) -from pandas.core.base import NoNewAttributesMixin +from pandas.core.arrays.base import ExtensionArray +from pandas.core.arrays.categorical import Categorical +from pandas.core.arrays.interval import IntervalArray +from pandas.core.arrays.timedeltas import TimedeltaArray +from pandas.core.base import ( + IndexOpsMixin, + NoNewAttributesMixin, +) from pandas.core.frame import DataFrame from pandas.core.series import ( PeriodSeries, Series, ) -from typing_extensions import Never +from pandas._libs.interval import Interval from pandas._libs.tslibs import BaseOffset from pandas._libs.tslibs.offsets import DateOffset from pandas._typing import ( - S1, TimeAmbiguous, TimeNonexistent, TimestampConvention, @@ -46,6 +53,8 @@ from pandas._typing import ( np_ndarray_bool, ) +from pandas.core.dtypes.dtypes import CategoricalDtype + class Properties(PandasDelegate, NoNewAttributesMixin): ... _DTFieldOpsReturnType = TypeVar("_DTFieldOpsReturnType", bound=Series[int] | Index[int]) @@ -129,10 +138,10 @@ class _DatetimeObjectOps( ): ... _DTOtherOpsDateReturnType = TypeVar( - "_DTOtherOpsDateReturnType", bound=Series[dt.date] | np_1darray[np.object_] + "_DTOtherOpsDateReturnType", bound=Series[date] | np_1darray[np.object_] ) _DTOtherOpsTimeReturnType = TypeVar( - "_DTOtherOpsTimeReturnType", bound=Series[dt.time] | np_1darray[np.object_] + "_DTOtherOpsTimeReturnType", bound=Series[time] | np_1darray[np.object_] ) class _DatetimeOtherOps(Generic[_DTOtherOpsDateReturnType, _DTOtherOpsTimeReturnType]): @@ -380,8 +389,8 @@ class CombinedDatetimelikeProperties( Series[int], Series[bool], Series, - Series[dt.date], - Series[dt.time], + Series[date], + Series[time], str, Series[Timestamp], Series[str], @@ -395,8 +404,8 @@ class TimestampProperties( Series[int], Series[bool], Series[Timestamp], - Series[dt.date], - Series[dt.time], + Series[date], + Series[time], str, Series[Timestamp], Series[str], @@ -434,49 +443,37 @@ class TimedeltaIndexProperties( _DatetimeRoundingMethods[TimedeltaIndex], ): ... -class _dtDescriptor(CombinedDatetimelikeProperties, Generic[S1]): - @overload - def __get__(self, instance: Series[Never], owner: Any) -> Never: ... +@type_check_only +class DtDescriptor: @overload def __get__( - self, instance: Series[Timestamp], owner: Any + self, instance: Series[Timestamp], owner: type[Series] ) -> TimestampProperties: ... @overload def __get__( - self, instance: Series[Timedelta], owner: Any + self, instance: Series[Timedelta], owner: type[Series] ) -> TimedeltaProperties: ... + +@type_check_only +class ArrayDescriptor: @overload def __get__( - self, instance: Series[S1], owner: Any - ) -> CombinedDatetimelikeProperties: ... - def round( - self, - freq: str | BaseOffset | None, - ambiguous: Literal["raise", "infer", "NaT"] | bool | np_ndarray_bool = ..., - nonexistent: ( - Literal["shift_forward", "shift_backward", "NaT", "raise"] - | timedelta - | Timedelta - ) = ..., - ) -> Series[S1]: ... - def floor( - self, - freq: str | BaseOffset | None, - ambiguous: Literal["raise", "infer", "NaT"] | bool | np_ndarray_bool = ..., - nonexistent: ( - Literal["shift_forward", "shift_backward", "NaT", "raise"] - | timedelta - | Timedelta - ) = ..., - ) -> Series[S1]: ... - def ceil( - self, - freq: str | BaseOffset | None, - ambiguous: Literal["raise", "infer", "NaT"] | bool | np_ndarray_bool = ..., - nonexistent: ( - Literal["shift_forward", "shift_backward", "NaT", "raise"] - | timedelta - | Timedelta - ) = ..., - ) -> Series[S1]: ... - def as_unit(self, unit: TimeUnit) -> Series[S1]: ... + self, instance: IndexOpsMixin[CategoricalDtype], owner: type[IndexOpsMixin] + ) -> Categorical: ... + @overload + def __get__( + self, instance: IndexOpsMixin[Interval], owner: type[IndexOpsMixin] + ) -> IntervalArray: ... + @overload + def __get__( + self, instance: IndexOpsMixin[Timestamp], owner: type[IndexOpsMixin] + ) -> DatetimeArray: ... + @overload + def __get__( + self, instance: IndexOpsMixin[Timedelta], owner: type[IndexOpsMixin] + ) -> TimedeltaArray: ... + # should be NumpyExtensionArray + @overload + def __get__( + self, instance: IndexOpsMixin, owner: type[IndexOpsMixin] + ) -> ExtensionArray: ... diff --git a/pandas-stubs/core/indexes/base.pyi b/pandas-stubs/core/indexes/base.pyi index d98bb6e15..df2f52efe 100644 --- a/pandas-stubs/core/indexes/base.pyi +++ b/pandas-stubs/core/indexes/base.pyi @@ -36,7 +36,6 @@ from pandas import ( Series, TimedeltaIndex, ) -from pandas.core.arrays import ExtensionArray from pandas.core.base import ( IndexOpsMixin, NumListLike, @@ -324,7 +323,7 @@ class Index(IndexOpsMixin[S1]): self, name: bool = ..., formatter: Callable | None = ..., na_rep: _str = ... ) -> list[_str]: ... def to_flat_index(self): ... - def to_series(self, index=..., name: Hashable = ...) -> Series: ... + def to_series(self, index=..., name: Hashable = ...) -> Series[S1]: ... def to_frame(self, index: bool = True, name=...) -> DataFrame: ... @property def name(self) -> Hashable | None: ... @@ -413,8 +412,6 @@ class Index(IndexOpsMixin[S1]): ): ... @property def values(self) -> np_1darray: ... - @property - def array(self) -> ExtensionArray: ... def memory_usage(self, deep: bool = False): ... def where(self, cond, other: Scalar | ArrayLike | None = None): ... def __contains__(self, key) -> bool: ... diff --git a/pandas-stubs/core/indexes/datetimes.pyi b/pandas-stubs/core/indexes/datetimes.pyi index 42b2f2d4d..8e343c7dc 100644 --- a/pandas-stubs/core/indexes/datetimes.pyi +++ b/pandas-stubs/core/indexes/datetimes.pyi @@ -20,7 +20,6 @@ from pandas import ( TimedeltaIndex, Timestamp, ) -from pandas.core.arrays import DatetimeArray from pandas.core.indexes.accessors import DatetimeIndexProperties from pandas.core.indexes.datetimelike import DatetimeTimedeltaMixin from pandas.core.series import Series @@ -60,10 +59,6 @@ class DatetimeIndex( ) -> Self: ... def __reduce__(self): ... - # Override the array property to return DatetimeArray instead of ExtensionArray - @property - def array(self) -> DatetimeArray: ... - # various ignores needed for mypy, as we do want to restrict what can be used in # arithmetic for these types def __add__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] diff --git a/pandas-stubs/core/indexes/period.pyi b/pandas-stubs/core/indexes/period.pyi index 158f38698..6558bb76c 100644 --- a/pandas-stubs/core/indexes/period.pyi +++ b/pandas-stubs/core/indexes/period.pyi @@ -11,6 +11,7 @@ from pandas import Index from pandas.core.indexes.accessors import PeriodIndexFieldOps from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin from pandas.core.indexes.timedeltas import TimedeltaIndex +from pandas.core.series import PeriodSeries from typing_extensions import Self from pandas._libs.tslibs import ( @@ -74,6 +75,7 @@ class PeriodIndex(DatetimeIndexOpsMixin[pd.Period, np.object_], PeriodIndexField @property def freqstr(self) -> str: ... def shift(self, periods: int = 1, freq: Frequency | None = None) -> Self: ... + def to_series(self, index=..., name: Hashable = ...) -> PeriodSeries: ... def period_range( start: ( diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 2af21e434..1fed35365 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -55,11 +55,9 @@ from pandas.core.api import ( Int32Dtype as Int32Dtype, Int64Dtype as Int64Dtype, ) -from pandas.core.arrays import TimedeltaArray -from pandas.core.arrays.base import ExtensionArray from pandas.core.arrays.categorical import CategoricalAccessor from pandas.core.arrays.datetimes import DatetimeArray -from pandas.core.arrays.interval import IntervalArray +from pandas.core.arrays.timedeltas import TimedeltaArray from pandas.core.base import ( IndexOpsMixin, NumListLike, @@ -71,8 +69,8 @@ from pandas.core.groupby.generic import SeriesGroupBy from pandas.core.groupby.groupby import BaseGroupBy from pandas.core.indexers import BaseIndexer from pandas.core.indexes.accessors import ( + DtDescriptor, PeriodProperties, - _dtDescriptor, ) from pandas.core.indexes.category import CategoricalIndex from pandas.core.indexes.datetimes import DatetimeIndex @@ -392,7 +390,7 @@ class Series(IndexOpsMixin[S1], NDFrame): dtype: Literal["Interval"] = ..., name: Hashable = ..., copy: bool = ..., - ) -> IntervalSeries[_OrderableT]: ... + ) -> Series[Interval[_OrderableT]]: ... @overload def __new__( # type: ignore[overload-overlap] cls, @@ -479,8 +477,6 @@ class Series(IndexOpsMixin[S1], NDFrame): def name(self, value: Hashable | None) -> None: ... @property def values(self) -> ArrayLike: ... - @property - def array(self) -> ExtensionArray: ... def ravel(self, order: _str = ...) -> np.ndarray: ... def __len__(self) -> int: ... def view(self, dtype=...) -> Series[S1]: ... @@ -841,11 +837,10 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def diff(self: Series[complex], periods: int = ...) -> Series[complex]: ... # type: ignore[overload-overlap] @overload - def diff(self: Series[bytes], periods: int = ...) -> Never: ... - @overload - def diff(self: Series[type], periods: int = ...) -> Never: ... - @overload - def diff(self: Series[_str], periods: int = ...) -> Never: ... + def diff( + self: Series[bytes] | Series[type] | Series[_str] | Series[Interval], + periods: int = ..., + ) -> Never: ... @overload def diff(self: Series[Timestamp], periods: int = ...) -> Series[Timedelta]: ... # type: ignore[overload-overlap] @overload @@ -1234,8 +1229,7 @@ class Series(IndexOpsMixin[S1], NDFrame): Series[_str], Series, ]: ... - @property - def dt(self) -> _dtDescriptor[S1]: ... + dt: ClassVar = DtDescriptor() # noqa @property def plot(self) -> PlotAccessor: ... sparse = ... @@ -4211,9 +4205,6 @@ class Series(IndexOpsMixin[S1], NDFrame): def __xor__(self, other: int | np_ndarray_anyint | Series[int]) -> Series[int]: ... @final def __invert__(self) -> Series[bool]: ... - # properties - # @property - # def array(self) -> _npndarray @property def at(self) -> _AtIndexer: ... @property @@ -4593,7 +4584,7 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload # type: ignore[override] def to_numpy( # pyrefly: ignore[bad-override] self: Series[Timestamp], - dtype: None | type[np.datetime64] = None, + dtype: type[np.datetime64] | None = None, copy: bool = False, na_value: Scalar = ..., **kwargs, @@ -4609,7 +4600,7 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def to_numpy( # pyrefly: ignore[bad-override] self: Series[Timedelta], - dtype: None | type[np.timedelta64] = None, + dtype: type[np.timedelta64] | None = None, copy: bool = False, na_value: Scalar = ..., **kwargs, @@ -4623,6 +4614,22 @@ class Series(IndexOpsMixin[S1], NDFrame): **kwargs, ) -> np_1darray[GenericT]: ... @overload + def to_numpy( + self: Series[Interval], + dtype: type[np.object_] | None = None, + copy: bool = False, + na_value: Scalar = ..., + **kwargs, + ) -> np_1darray[np.object_]: ... + @overload + def to_numpy( + self: Series[Interval], + dtype: np.dtype[GenericT] | SupportsDType[GenericT] | type[GenericT], + copy: bool = False, + na_value: Scalar = ..., + **kwargs, + ) -> np_1darray[GenericT]: ... + @overload def to_numpy( # pyright: ignore[reportIncompatibleMethodOverride] self, dtype: DTypeLike | None = None, @@ -4718,7 +4725,7 @@ class _SeriesSubclassBase(Series[S1], Generic[S1, GenericT_co]): class PeriodSeries(_SeriesSubclassBase[Period, np.object_]): @property - def dt(self) -> PeriodProperties: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + def dt(self) -> PeriodProperties: ... # type: ignore[override] def __sub__(self, other: PeriodSeries) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] def diff(self, periods: int = ...) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] @@ -4729,10 +4736,3 @@ class OffsetSeries(_SeriesSubclassBase[BaseOffset, np.object_]): def __radd__( # pyright: ignore[reportIncompatibleMethodOverride] self, other: BaseOffset ) -> OffsetSeries: ... - -class IntervalSeries( - _SeriesSubclassBase[Interval[_OrderableT], np.object_], Generic[_OrderableT] -): - @property - def array(self) -> IntervalArray: ... - def diff(self, periods: int = ...) -> Never: ... # pyrefly: ignore diff --git a/tests/indexes/test_indexes.py b/tests/indexes/test_indexes.py index eb6a790b4..11feaaa61 100644 --- a/tests/indexes/test_indexes.py +++ b/tests/indexes/test_indexes.py @@ -12,8 +12,11 @@ import numpy as np from numpy import typing as npt import pandas as pd -from pandas.core.arrays import DatetimeArray +from pandas.core.arrays.base import ExtensionArray from pandas.core.arrays.categorical import Categorical +from pandas.core.arrays.datetimes import DatetimeArray +from pandas.core.arrays.interval import IntervalArray +from pandas.core.arrays.timedeltas import TimedeltaArray from pandas.core.indexes.base import Index from typing_extensions import ( Never, @@ -29,6 +32,8 @@ ) if TYPE_CHECKING: + from pandas.core.series import PeriodSeries # noqa: F401 + from tests import Dtype # noqa: F401 @@ -1519,12 +1524,77 @@ def test_period_index_asof_locs() -> None: ) -def test_datetime_index_array_property() -> None: - """Test that DatetimeIndex.array returns DatetimeArray instead of ExtensionArray.""" +def test_array_property() -> None: + """Test that Index.array and semantic Index.array return ExtensionArray and its subclasses""" + # pandas-dev/pandas-stubs#1383 + # check(assert_type(Index([1], dtype="category").array, pd.Categorical), pd.Categorical, np.int64) + check( + assert_type(pd.interval_range(0, 1).array, IntervalArray), + IntervalArray, + pd.Interval, + ) + # Test with pd.to_datetime().array - this is the main issue reported arr = pd.to_datetime(["2020-01-01", "2020-01-02"]).array - check(assert_type(arr, DatetimeArray), DatetimeArray) + check(assert_type(arr, DatetimeArray), DatetimeArray, pd.Timestamp) # Test with DatetimeIndex constructor directly dt_index = pd.DatetimeIndex(["2020-01-01", "2020-01-02"]) - check(assert_type(dt_index.array, DatetimeArray), DatetimeArray) + check(assert_type(dt_index.array, DatetimeArray), DatetimeArray, pd.Timestamp) + + check( + assert_type(pd.to_timedelta(["1s"]).array, TimedeltaArray), + TimedeltaArray, + pd.Timedelta, + ) + # Should be NumpyExtensionArray + check(assert_type(Index([1]).array, ExtensionArray), ExtensionArray, np.integer) + + +def test_to_series() -> None: + """Test that Index.to_series return typed Series""" + check( + assert_type(pd.interval_range(0, 1).to_series(), "pd.Series[pd.Interval[int]]"), + pd.Series, + pd.Interval, + ) + check( + assert_type( + pd.date_range(start="2022-06-01", periods=10).to_series(), + "pd.Series[pd.Timestamp]", + ), + pd.Series, + pd.Timestamp, + ) + + check( + assert_type( + pd.timedelta_range(start="1 day", periods=10).to_series(), + "pd.Series[pd.Timedelta]", + ), + pd.Series, + pd.Timedelta, + ) + check( + assert_type( + pd.period_range(start="2022-06-01", periods=10).to_series(), "PeriodSeries" + ), + pd.Series, + pd.Period, + ) + + check( + assert_type(Index([True]).to_series(), "pd.Series[bool]"), pd.Series, np.bool_ + ) + check(assert_type(Index([1]).to_series(), "pd.Series[int]"), pd.Series, np.integer) + check( + assert_type(Index([1.0]).to_series(), "pd.Series[float]"), + pd.Series, + np.floating, + ) + check( + assert_type(Index([1j]).to_series(), "pd.Series[complex]"), + pd.Series, + np.complexfloating, + ) + check(assert_type(Index(["1"]).to_series(), "pd.Series[str]"), pd.Series, str) diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py new file mode 100644 index 000000000..23422fb30 --- /dev/null +++ b/tests/series/test_properties.py @@ -0,0 +1,62 @@ +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from pandas.core.arrays import DatetimeArray +from pandas.core.arrays.base import ExtensionArray +from pandas.core.arrays.interval import IntervalArray +from pandas.core.arrays.timedeltas import TimedeltaArray +from pandas.core.indexes.accessors import ( + DatetimeProperties, + TimedeltaProperties, +) +from typing_extensions import assert_type + +from tests import ( + TYPE_CHECKING_INVALID_USAGE, + check, +) + +if TYPE_CHECKING: + from pandas.core.indexes.accessors import ( # noqa: F401 + TimedeltaProperties, + TimestampProperties, + ) + + +def test_dt_property() -> None: + """Test the Series.dt property""" + check( + assert_type(pd.Series([pd.Timestamp(2025, 9, 28)]).dt, "TimestampProperties"), + DatetimeProperties, + ) + check( + assert_type(pd.Series([pd.Timedelta(1, "s")]).dt, TimedeltaProperties), + TimedeltaProperties, + ) + + if TYPE_CHECKING_INVALID_USAGE: + _ = pd.Series([1]).dt # type: ignore[arg-type] # pyright: ignore[reportAttributeAccessIssue] + + +def test_array_property() -> None: + """Test that Series.array returns ExtensionArray and its subclasses""" + # pandas-dev/pandas-stubs#1383 + # check(assert_type(pd.Series([1], dtype="category").array, pd.Categorical), pd.Categorical, np.int64) + check( + assert_type(pd.Series(pd.interval_range(0, 1)).array, IntervalArray), + IntervalArray, + pd.Interval, + ) + check( + assert_type(pd.Series([pd.Timestamp(2025, 9, 28)]).array, DatetimeArray), + DatetimeArray, + pd.Timestamp, + ) + check( + assert_type(pd.Series([pd.Timedelta(1, "s")]).array, TimedeltaArray), + TimedeltaArray, + pd.Timedelta, + ) + # Should be NumpyExtensionArray + check(assert_type(pd.Series([1]).array, ExtensionArray), ExtensionArray, np.integer) diff --git a/tests/test_extension.py b/tests/test_extension.py index a8e34c171..49c197c8b 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -24,7 +24,7 @@ def test_tolist() -> None: s = pd.Series(data) data1 = [1, 2, 3] s1 = pd.Series(data1) - check(assert_type(s.array.tolist(), list), list) + check(assert_type(s.array.tolist(), list), list) # type: ignore[assert-type] check(assert_type(s1.array.tolist(), list), list) check(assert_type(pd.array([1, 2, 3]).tolist(), list), list) diff --git a/tests/test_timefuncs.py b/tests/test_timefuncs.py index 354be303b..3e1e2b80c 100644 --- a/tests/test_timefuncs.py +++ b/tests/test_timefuncs.py @@ -51,7 +51,6 @@ if TYPE_CHECKING: from pandas.core.series import ( # noqa: F401 - IntervalSeries, OffsetSeries, PeriodSeries, ) @@ -357,10 +356,6 @@ def test_series_dt_accessors() -> None: i0 = pd.date_range(start="2022-06-01", periods=10) check(assert_type(i0, pd.DatetimeIndex), pd.DatetimeIndex, pd.Timestamp) - check( - assert_type(i0.to_series(), "pd.Series[pd.Timestamp]"), pd.Series, pd.Timestamp - ) - s0 = pd.Series(i0) check(assert_type(s0.dt.date, "pd.Series[dt.date]"), pd.Series, dt.date) @@ -531,8 +526,6 @@ def test_series_dt_accessors() -> None: check(assert_type(i1, pd.PeriodIndex), pd.PeriodIndex) - check(assert_type(i1.to_series(), pd.Series), pd.Series, pd.Period) - s1 = pd.Series(i1) check(assert_type(s1.dt.qyear, "pd.Series[int]"), pd.Series, np.integer) @@ -948,7 +941,7 @@ def test_series_types_to_numpy() -> None: o_s = cast( "OffsetSeries", pd.Series([pd.DateOffset(days=1), pd.DateOffset(days=2)]) ) - i_s = cast("IntervalSeries", pd.interval_range(1, 2).to_series()) + i_s = pd.interval_range(1, 2).to_series() # default dtype check( From 0ba9781bb3d262baaddef54198399cb7a660d86d Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sun, 28 Sep 2025 22:01:58 +0200 Subject: [PATCH 02/14] chore(import): simplify --- tests/series/test_properties.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 23422fb30..7f3cadfc7 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -18,10 +18,7 @@ ) if TYPE_CHECKING: - from pandas.core.indexes.accessors import ( # noqa: F401 - TimedeltaProperties, - TimestampProperties, - ) + from pandas.core.indexes.accessors import TimestampProperties # noqa: F401 def test_dt_property() -> None: From 7cd514e6338c74e0f8c42669425daaf3eed4007e Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sun, 28 Sep 2025 22:50:41 +0200 Subject: [PATCH 03/14] fix: comments --- pandas-stubs/core/indexes/accessors.pyi | 9 +++++++++ pyproject.toml | 6 +++--- tests/indexes/test_indexes.py | 1 - tests/series/test_properties.py | 7 +++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index 4414f706c..ab95af0c0 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -7,6 +7,7 @@ from datetime import ( from typing import ( Generic, Literal, + Never, TypeVar, overload, type_check_only, @@ -399,6 +400,8 @@ class CombinedDatetimelikeProperties( _TimedeltaPropertiesNoRounding[Series[int], Series[float]], _PeriodProperties, ): ... + +@type_check_only class TimestampProperties( DatetimeProperties[ Series[int], @@ -445,6 +448,8 @@ class TimedeltaIndexProperties( @type_check_only class DtDescriptor: + @overload + def __get__(self, instance: Series[Never], owner: type[Series]) -> Never: ... @overload def __get__( self, instance: Series[Timestamp], owner: type[Series] @@ -456,6 +461,10 @@ class DtDescriptor: @type_check_only class ArrayDescriptor: + @overload + def __get__( + self, instance: IndexOpsMixin[Never], owner: type[IndexOpsMixin] + ) -> ExtensionArray: ... @overload def __get__( self, instance: IndexOpsMixin[CategoricalDtype], owner: type[IndexOpsMixin] diff --git a/pyproject.toml b/pyproject.toml index 5734743ff..f82284a7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,16 +39,16 @@ numpy = ">= 1.23.5" mypy = "1.18.2" pandas = "2.3.2" pyarrow = ">=10.0.1" -pytest = ">=7.1.2" +pytest = ">=8.4.2" pyright = ">=1.1.405" ty = ">=0.0.1a21" -pyrefly = ">=0.33.1" +pyrefly = ">=0.34.0" poethepoet = ">=0.16.5" loguru = ">=0.6.0" typing-extensions = ">=4.4.0" matplotlib = ">=3.10.1" pre-commit = ">=2.19.0" -black = ">=25.1.0" +black = ">=25.9.0" isort = ">=6.0.1" openpyxl = ">=3.0.10" tables = { version = ">=3.10.1", python = "<4" } diff --git a/tests/indexes/test_indexes.py b/tests/indexes/test_indexes.py index 11feaaa61..e60c5e92c 100644 --- a/tests/indexes/test_indexes.py +++ b/tests/indexes/test_indexes.py @@ -1547,7 +1547,6 @@ def test_array_property() -> None: TimedeltaArray, pd.Timedelta, ) - # Should be NumpyExtensionArray check(assert_type(Index([1]).array, ExtensionArray), ExtensionArray, np.integer) diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 7f3cadfc7..041ba1389 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -33,7 +33,9 @@ def test_dt_property() -> None: ) if TYPE_CHECKING_INVALID_USAGE: - _ = pd.Series([1]).dt # type: ignore[arg-type] # pyright: ignore[reportAttributeAccessIssue] + # mypy gives Any + _0 = pd.Series([1, "s"]).dt # pyright: ignore[reportAttributeAccessIssue] + _1 = pd.Series([1]).dt # type: ignore[arg-type,var-annotated] # pyright: ignore[reportAttributeAccessIssue] def test_array_property() -> None: @@ -55,5 +57,6 @@ def test_array_property() -> None: TimedeltaArray, pd.Timedelta, ) - # Should be NumpyExtensionArray check(assert_type(pd.Series([1]).array, ExtensionArray), ExtensionArray, np.integer) + # mypy gives Any + check(assert_type(pd.Series([1, "s"]).array, ExtensionArray), ExtensionArray) # type: ignore[assert-type] From 8ab6edd21f4743bc94b3740fae57304f6a42bb2c Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Sun, 28 Sep 2025 22:55:50 +0200 Subject: [PATCH 04/14] fix(import): py310 --- pandas-stubs/core/indexes/accessors.pyi | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index ab95af0c0..d34796579 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -7,43 +7,39 @@ from datetime import ( from typing import ( Generic, Literal, - Never, TypeVar, overload, type_check_only, ) import numpy as np -from pandas import ( - DatetimeIndex, - Index, - PeriodIndex, - Timedelta, - TimedeltaIndex, - Timestamp, -) from pandas.core.accessor import PandasDelegate -from pandas.core.arrays import ( - DatetimeArray, - PeriodArray, -) from pandas.core.arrays.base import ExtensionArray from pandas.core.arrays.categorical import Categorical +from pandas.core.arrays.datetimes import DatetimeArray from pandas.core.arrays.interval import IntervalArray +from pandas.core.arrays.period import PeriodArray from pandas.core.arrays.timedeltas import TimedeltaArray from pandas.core.base import ( IndexOpsMixin, NoNewAttributesMixin, ) from pandas.core.frame import DataFrame +from pandas.core.indexes.base import Index +from pandas.core.indexes.datetimes import DatetimeIndex +from pandas.core.indexes.period import PeriodIndex +from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.core.series import ( PeriodSeries, Series, ) +from typing_extensions import Never from pandas._libs.interval import Interval from pandas._libs.tslibs import BaseOffset from pandas._libs.tslibs.offsets import DateOffset +from pandas._libs.tslibs.timedeltas import Timedelta +from pandas._libs.tslibs.timestamps import Timestamp from pandas._typing import ( TimeAmbiguous, TimeNonexistent, From 9eb2ed8addf9abe2eb5394243bfb179c6b071502 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 10:28:47 +0200 Subject: [PATCH 05/14] fix: comments --- pandas-stubs/core/indexes/accessors.pyi | 4 +++- pandas-stubs/core/series.pyi | 6 +++--- pyproject.toml | 2 +- tests/series/test_properties.py | 10 ++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index d34796579..de0c25566 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -5,6 +5,7 @@ from datetime import ( tzinfo as _tzinfo, ) from typing import ( + Any, Generic, Literal, TypeVar, @@ -444,8 +445,9 @@ class TimedeltaIndexProperties( @type_check_only class DtDescriptor: + # microsoft/pyright#10924 @overload - def __get__(self, instance: Series[Never], owner: type[Series]) -> Never: ... + def __get__(self, instance: Series[Never], owner: type[Series]) -> Any: ... @overload def __get__( self, instance: Series[Timestamp], owner: type[Series] diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 1fed35365..d1e9efd05 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -4624,11 +4624,11 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def to_numpy( self: Series[Interval], - dtype: np.dtype[GenericT] | SupportsDType[GenericT] | type[GenericT], + dtype: np.dtype[np.bytes_], copy: bool = False, na_value: Scalar = ..., **kwargs, - ) -> np_1darray[GenericT]: ... + ) -> np_1darray[np.bytes_]: ... @overload def to_numpy( # pyright: ignore[reportIncompatibleMethodOverride] self, @@ -4725,7 +4725,7 @@ class _SeriesSubclassBase(Series[S1], Generic[S1, GenericT_co]): class PeriodSeries(_SeriesSubclassBase[Period, np.object_]): @property - def dt(self) -> PeriodProperties: ... # type: ignore[override] + def dt(self) -> PeriodProperties: ... def __sub__(self, other: PeriodSeries) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] def diff(self, periods: int = ...) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] diff --git a/pyproject.toml b/pyproject.toml index f82284a7d..b378e3500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ scipy = { version = ">=1.9.1", python = "<3.14" } scipy-stubs = ">=1.15.3.0" SQLAlchemy = ">=2.0.39" types-python-dateutil = ">=2.8.19" -beautifulsoup4 = ">=4.12.2" +beautifulsoup4 = ">=4.12.2,!=4.14.0" # pandas-dev/pandas#62492 html5lib = ">=1.1" python-calamine = ">=0.2.0" diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 041ba1389..541708663 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -1,4 +1,7 @@ -from typing import TYPE_CHECKING +from typing import ( + TYPE_CHECKING, + Any, +) import numpy as np import pandas as pd @@ -33,9 +36,8 @@ def test_dt_property() -> None: ) if TYPE_CHECKING_INVALID_USAGE: - # mypy gives Any - _0 = pd.Series([1, "s"]).dt # pyright: ignore[reportAttributeAccessIssue] - _1 = pd.Series([1]).dt # type: ignore[arg-type,var-annotated] # pyright: ignore[reportAttributeAccessIssue] + assert_type(pd.DataFrame({"a": [1]})["a"].dt, Any) + _1 = pd.Series([1]).dt # type: ignore[arg-type] # pyright: ignore[reportAttributeAccessIssue] def test_array_property() -> None: From 6100ccf97d72502df309836baedb85f3cab6f039 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 10:32:51 +0200 Subject: [PATCH 06/14] fix: comment --- pandas-stubs/core/series.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index d1e9efd05..cc009142f 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -4624,7 +4624,7 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def to_numpy( self: Series[Interval], - dtype: np.dtype[np.bytes_], + dtype: type[np.bytes_], copy: bool = False, na_value: Scalar = ..., **kwargs, From 46eea6f0dcf1fd8ee155ce9de042ba749d0204d4 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 10:40:27 +0200 Subject: [PATCH 07/14] fix: pyrefly --- pandas-stubs/core/indexes/accessors.pyi | 7 +++++++ pandas-stubs/core/series.pyi | 4 +--- tests/series/test_properties.py | 8 ++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index de0c25566..82fc4e8bf 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -39,6 +39,7 @@ from typing_extensions import Never from pandas._libs.interval import Interval from pandas._libs.tslibs import BaseOffset from pandas._libs.tslibs.offsets import DateOffset +from pandas._libs.tslibs.period import Period from pandas._libs.tslibs.timedeltas import Timedelta from pandas._libs.tslibs.timestamps import Timestamp from pandas._typing import ( @@ -456,6 +457,12 @@ class DtDescriptor: def __get__( self, instance: Series[Timedelta], owner: type[Series] ) -> TimedeltaProperties: ... + @overload + def __get__( + self, + instance: Series[Period] | PeriodSeries, + owner: type[Series | PeriodSeries], + ) -> PeriodProperties: ... @type_check_only class ArrayDescriptor: diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index cc009142f..9b604edcf 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -70,7 +70,6 @@ from pandas.core.groupby.groupby import BaseGroupBy from pandas.core.indexers import BaseIndexer from pandas.core.indexes.accessors import ( DtDescriptor, - PeriodProperties, ) from pandas.core.indexes.category import CategoricalIndex from pandas.core.indexes.datetimes import DatetimeIndex @@ -4724,8 +4723,7 @@ class _SeriesSubclassBase(Series[S1], Generic[S1, GenericT_co]): ) -> np_1darray: ... class PeriodSeries(_SeriesSubclassBase[Period, np.object_]): - @property - def dt(self) -> PeriodProperties: ... + dt: ClassVar = DtDescriptor() # noqa def __sub__(self, other: PeriodSeries) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] def diff(self, periods: int = ...) -> OffsetSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 541708663..e2db4d7cf 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -11,6 +11,7 @@ from pandas.core.arrays.timedeltas import TimedeltaArray from pandas.core.indexes.accessors import ( DatetimeProperties, + PeriodProperties, TimedeltaProperties, ) from typing_extensions import assert_type @@ -34,6 +35,13 @@ def test_dt_property() -> None: assert_type(pd.Series([pd.Timedelta(1, "s")]).dt, TimedeltaProperties), TimedeltaProperties, ) + check( + assert_type( + pd.period_range(start="2022-06-01", periods=10).to_series().dt, + PeriodProperties, + ), + PeriodProperties, + ) if TYPE_CHECKING_INVALID_USAGE: assert_type(pd.DataFrame({"a": [1]})["a"].dt, Any) From 69cb0cbfd6fda18654ea68dd9ff84d5a454dcbf9 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 19:30:31 +0200 Subject: [PATCH 08/14] fix: comments --- pandas-stubs/core/base.pyi | 3 +-- pandas-stubs/core/indexes/accessors.pyi | 5 +---- pandas-stubs/core/series.pyi | 2 +- tests/series/test_properties.py | 7 +++++-- tests/test_extension.py | 2 ++ 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pandas-stubs/core/base.pyi b/pandas-stubs/core/base.pyi index 3aa09dc57..4b6e2e05d 100644 --- a/pandas-stubs/core/base.pyi +++ b/pandas-stubs/core/base.pyi @@ -5,7 +5,6 @@ from collections.abc import ( ) from typing import ( Any, - ClassVar, Generic, Literal, TypeAlias, @@ -71,7 +70,7 @@ class IndexOpsMixin(OpsMixin, Generic[S1, GenericT_co]): def nbytes(self) -> int: ... @property def size(self) -> int: ... - array: ClassVar = ArrayDescriptor() # noqa + array = ArrayDescriptor() @overload def to_numpy( self, diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index 962b9ecf8..4d0907461 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -443,7 +443,6 @@ class TimedeltaIndexProperties( @type_check_only class DtDescriptor: - # microsoft/pyright#10924 @overload def __get__(self, instance: Series[Never], owner: type[Series]) -> Any: ... @overload @@ -456,9 +455,7 @@ class DtDescriptor: ) -> TimedeltaProperties: ... @overload def __get__( - self, - instance: Series[Period], - owner: type[Series], + self, instance: Series[Period], owner: type[Series] ) -> PeriodProperties: ... @type_check_only diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index f238d2a7e..151135aac 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -1230,7 +1230,7 @@ class Series(IndexOpsMixin[S1], NDFrame): Series[_str], Series, ]: ... - dt: ClassVar = DtDescriptor() # noqa + dt = DtDescriptor() @property def plot(self) -> PlotAccessor: ... sparse = ... diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index e2db4d7cf..97b4192a6 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -44,7 +44,9 @@ def test_dt_property() -> None: ) if TYPE_CHECKING_INVALID_USAGE: - assert_type(pd.DataFrame({"a": [1]})["a"].dt, Any) + s = pd.DataFrame({"a": [1]})["a"] + assert_type(s.dt, Any) + assert_type(s.dt.year, Any) _1 = pd.Series([1]).dt # type: ignore[arg-type] # pyright: ignore[reportAttributeAccessIssue] @@ -68,5 +70,6 @@ def test_array_property() -> None: pd.Timedelta, ) check(assert_type(pd.Series([1]).array, ExtensionArray), ExtensionArray, np.integer) - # mypy gives Any + # python/mypy#19952: mypy believes ExtensionArray and its subclasses have a + # conflict and gives Any for s.array check(assert_type(pd.Series([1, "s"]).array, ExtensionArray), ExtensionArray) # type: ignore[assert-type] diff --git a/tests/test_extension.py b/tests/test_extension.py index 49c197c8b..a12f264d2 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -24,6 +24,8 @@ def test_tolist() -> None: s = pd.Series(data) data1 = [1, 2, 3] s1 = pd.Series(data1) + # python/mypy#19952: mypy believes ExtensionArray and its subclasses have a + # conflict and gives Any for s.array check(assert_type(s.array.tolist(), list), list) # type: ignore[assert-type] check(assert_type(s1.array.tolist(), list), list) check(assert_type(pd.array([1, 2, 3]).tolist(), list), list) From aef9fb701098cf1592fb1f048e39e7abd4e7d196 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 20:12:30 +0200 Subject: [PATCH 09/14] feat: comments --- pandas-stubs/core/series.pyi | 32 ++++++++++++++++------------ tests/series/test_series.py | 41 ++++++++++++++---------------------- tests/test_timefuncs.py | 5 +++++ 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 151135aac..187c37c0f 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -27,6 +27,7 @@ from typing import ( Literal, NoReturn, Protocol, + TypeVar, final, overload, type_check_only, @@ -210,10 +211,16 @@ from pandas.core.dtypes.dtypes import CategoricalDtype from pandas.plotting import PlotAccessor +_T_INTERVAL_NP = TypeVar("_T_INTERVAL_NP", bound=np.bytes_ | np.str_) + @type_check_only class _SupportsAdd(Protocol[_T_co]): def __add__(self, value: Self, /) -> _T_co: ... +@type_check_only +class SupportsSelfSub(Protocol[_T_co]): + def __sub__(self, x: Self, /) -> _T_co: ... + @type_check_only class _SupportsMul(Protocol[_T_co]): def __mul__(self, value: Self, /) -> _T_co: ... @@ -832,22 +839,19 @@ class Series(IndexOpsMixin[S1], NDFrame): self, other: Series[S1], min_periods: int | None = None, ddof: int = 1 ) -> float: ... @overload - def diff(self: Series[_bool], periods: int = ...) -> Series[type[object]]: ... # type: ignore[overload-overlap] - @overload - def diff(self: Series[complex], periods: int = ...) -> Series[complex]: ... # type: ignore[overload-overlap] - @overload - def diff( - self: Series[bytes] | Series[type] | Series[_str] | Series[Interval], - periods: int = ..., - ) -> Never: ... + def diff( # type: ignore[overload-overlap] + self: Series[Never] | Series[int], periods: int = ... + ) -> Series[float]: ... @overload - def diff(self: Series[Timestamp], periods: int = ...) -> Series[Timedelta]: ... # type: ignore[overload-overlap] + def diff(self: Series[_bool], periods: int = ...) -> Series: ... @overload - def diff(self: Series[Timedelta], periods: int = ...) -> Series[Timedelta]: ... # type: ignore[overload-overlap] + def diff(self: Series[Period], periods: int = ...) -> OffsetSeries: ... @overload - def diff(self: Series[Period], periods: int = ...) -> OffsetSeries: ... # type: ignore[overload-overlap] + def diff(self: Series[Interval], periods: int = ...) -> Never: ... @overload - def diff(self, periods: int = ...) -> Series[float]: ... + def diff( + self: SupportsGetItem[Scalar, SupportsSelfSub[S1_CO]], periods: int = ... + ) -> Series[S1_CO]: ... def autocorr(self, lag: int = 1) -> float: ... @overload def dot(self, other: Series[S1]) -> Scalar: ... @@ -4665,11 +4669,11 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def to_numpy( self: Series[Interval], - dtype: type[np.bytes_], + dtype: type[_T_INTERVAL_NP], copy: bool = False, na_value: Scalar = ..., **kwargs, - ) -> np_1darray[np.bytes_]: ... + ) -> np_1darray[_T_INTERVAL_NP]: ... @overload def to_numpy( # pyright: ignore[reportIncompatibleMethodOverride] self, diff --git a/tests/series/test_series.py b/tests/series/test_series.py index 8160e0baa..da9d9203c 100644 --- a/tests/series/test_series.py +++ b/tests/series/test_series.py @@ -40,7 +40,6 @@ Never, Self, TypeAlias, - assert_never, assert_type, ) import xarray as xr @@ -3503,47 +3502,39 @@ def test_diff() -> None: BaseOffset, index_to_check_for_type=-1, ) - # bool -> object + # bool -> Any check( assert_type( - pd.Series([True, True, False, False, True]).diff(), - "pd.Series[type[object]]", + pd.Series([True, True, False, False, True]).diff(), "pd.Series[Any]" ), pd.Series, object, ) - # object -> object - check( - assert_type(s.astype(object).diff(), "pd.Series[type[object]]"), - pd.Series, - object, - ) + # Any -> float + s_o = s.astype(object) + assert_type(s_o, "pd.Series[Any]") + check(assert_type(s_o.diff(), "pd.Series[float]"), pd.Series, float) # complex -> complex check( assert_type(s.astype(complex).diff(), "pd.Series[complex]"), pd.Series, complex ) - if TYPE_CHECKING_INVALID_USAGE: - # interval -> TypeError: IntervalArray has no 'diff' method. Convert to a suitable dtype prior to calling 'diff'. - assert_never(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff()) + def _diff_invalid0(): # pyright: ignore[reportUnusedFunction] + # interval -> TypeError: IntervalArray has no 'diff' method. Convert to a suitable dtype prior to calling 'diff'. + assert_type(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff(), Never) -def test_diff_never1() -> None: - s = pd.Series([1, 1, 2, 3, 5, 8]) - if TYPE_CHECKING_INVALID_USAGE: + def _diff_invalid1() -> None: # pyright: ignore[reportUnusedFunction] + s = pd.Series([1, 1, 2, 3, 5, 8]) # bytes -> numpy.core._exceptions._UFuncNoLoopError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('S21'), dtype('S21')) -> None - assert_never(s.astype(bytes).diff()) - + s.astype(bytes).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] -def test_diff_never2() -> None: - if TYPE_CHECKING_INVALID_USAGE: + def _diff_invalid2() -> None: # pyright: ignore[reportUnusedFunction] # dtype -> TypeError: unsupported operand type(s) for -: 'type' and 'type' - assert_never(pd.Series([str, int, bool]).diff()) - + pd.Series([str, int, bool]).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] -def test_diff_never3() -> None: - if TYPE_CHECKING_INVALID_USAGE: + def _diff_invalid3() -> None: # pyright: ignore[reportUnusedFunction] # str -> TypeError: unsupported operand type(s) for -: 'str' and 'str' - assert_never(pd.Series(["a", "b"]).diff()) + pd.Series(["a", "b"]).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] def test_operator_constistency() -> None: diff --git a/tests/test_timefuncs.py b/tests/test_timefuncs.py index fabd8d93b..a13a4686f 100644 --- a/tests/test_timefuncs.py +++ b/tests/test_timefuncs.py @@ -1063,6 +1063,11 @@ def test_series_types_to_numpy() -> None: np_1darray, dtype=np.bytes_, ) + check( + assert_type(i_s.to_numpy(dtype=np.str_), np_1darray[np.str_]), + np_1darray, + dtype=np.str_, + ) def test_index_types_to_numpy() -> None: From 245315c404d1319ff5246f8a6dee8d31c8b1e19f Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 20:30:57 +0200 Subject: [PATCH 10/14] chore: one line less --- tests/series/test_series.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/series/test_series.py b/tests/series/test_series.py index da9d9203c..db7e363b1 100644 --- a/tests/series/test_series.py +++ b/tests/series/test_series.py @@ -3524,9 +3524,8 @@ def _diff_invalid0(): # pyright: ignore[reportUnusedFunction] assert_type(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff(), Never) def _diff_invalid1() -> None: # pyright: ignore[reportUnusedFunction] - s = pd.Series([1, 1, 2, 3, 5, 8]) # bytes -> numpy.core._exceptions._UFuncNoLoopError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('S21'), dtype('S21')) -> None - s.astype(bytes).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] + pd.Series([1, 1, 2, 3, 5, 8]).astype(bytes).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] def _diff_invalid2() -> None: # pyright: ignore[reportUnusedFunction] # dtype -> TypeError: unsupported operand type(s) for -: 'type' and 'type' From e411ea0ea10a6d1ae6232534c77a04a6daf7fcd0 Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Mon, 29 Sep 2025 20:36:49 +0200 Subject: [PATCH 11/14] chore: one line less --- pandas-stubs/core/series.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 187c37c0f..956b22699 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -69,9 +69,7 @@ from pandas.core.generic import NDFrame from pandas.core.groupby.generic import SeriesGroupBy from pandas.core.groupby.groupby import BaseGroupBy from pandas.core.indexers import BaseIndexer -from pandas.core.indexes.accessors import ( - DtDescriptor, -) +from pandas.core.indexes.accessors import DtDescriptor from pandas.core.indexes.category import CategoricalIndex from pandas.core.indexes.datetimes import DatetimeIndex from pandas.core.indexes.interval import IntervalIndex From 0a9f5a2dd48d67bbdc71df5a14af1fa65dc5543c Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Tue, 30 Sep 2025 00:00:31 +0200 Subject: [PATCH 12/14] fix: comments --- pandas-stubs/core/indexes/accessors.pyi | 3 +-- pandas-stubs/core/series.pyi | 5 ++++ tests/scalars/__init__.py | 0 tests/{ => scalars}/test_scalars.py | 10 ++++++++ tests/series/test_properties.py | 7 +++--- tests/series/test_series.py | 31 ++++++++++++++++++------- 6 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 tests/scalars/__init__.py rename tests/{ => scalars}/test_scalars.py (99%) diff --git a/pandas-stubs/core/indexes/accessors.pyi b/pandas-stubs/core/indexes/accessors.pyi index 4d0907461..0cc058af7 100644 --- a/pandas-stubs/core/indexes/accessors.pyi +++ b/pandas-stubs/core/indexes/accessors.pyi @@ -5,7 +5,6 @@ from datetime import ( tzinfo as _tzinfo, ) from typing import ( - Any, Generic, Literal, TypeVar, @@ -444,7 +443,7 @@ class TimedeltaIndexProperties( @type_check_only class DtDescriptor: @overload - def __get__(self, instance: Series[Never], owner: type[Series]) -> Any: ... + def __get__(self, instance: Series[Never], owner: type[Series]) -> Properties: ... @overload def __get__( self, instance: Series[Timestamp], owner: type[Series] diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 956b22699..e56da2a13 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -56,6 +56,7 @@ from pandas.core.api import ( Int32Dtype as Int32Dtype, Int64Dtype as Int64Dtype, ) +from pandas.core.arrays.boolean import BooleanDtype from pandas.core.arrays.categorical import CategoricalAccessor from pandas.core.arrays.datetimes import DatetimeArray from pandas.core.arrays.timedeltas import TimedeltaArray @@ -843,6 +844,10 @@ class Series(IndexOpsMixin[S1], NDFrame): @overload def diff(self: Series[_bool], periods: int = ...) -> Series: ... @overload + def diff( + self: Series[BooleanDtype], periods: int = ... + ) -> Series[BooleanDtype]: ... + @overload def diff(self: Series[Period], periods: int = ...) -> OffsetSeries: ... @overload def diff(self: Series[Interval], periods: int = ...) -> Never: ... diff --git a/tests/scalars/__init__.py b/tests/scalars/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_scalars.py b/tests/scalars/test_scalars.py similarity index 99% rename from tests/test_scalars.py rename to tests/scalars/test_scalars.py index 3eb8f7cc4..54fcb08c5 100644 --- a/tests/test_scalars.py +++ b/tests/scalars/test_scalars.py @@ -287,6 +287,16 @@ def test_interval_math() -> None: pd.Interval, ) + if TYPE_CHECKING_INVALID_USAGE: + _i = interval_i - pd.Interval(1, 2) # type: ignore[type-var] # pyright: ignore[reportOperatorIssue] + _f = interval_f - pd.Interval(1.0, 2.0) # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + _ts = interval_ts - pd.Interval( # type: ignore[operator] + pd.Timestamp(2025, 9, 29), pd.Timestamp(2025, 9, 30), closed="both" + ) # pyright: ignore[reportOperatorIssue] + _td = interval_td - pd.Interval( # type: ignore[operator] + pd.Timedelta(1, "ns"), pd.Timedelta(2, "ns") + ) # pyright: ignore[reportOperatorIssue] + def test_interval_cmp(): interval_i = pd.Interval(0, 1, closed="left") diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 97b4192a6..9e5229f71 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -1,6 +1,5 @@ from typing import ( TYPE_CHECKING, - Any, ) import numpy as np @@ -12,6 +11,7 @@ from pandas.core.indexes.accessors import ( DatetimeProperties, PeriodProperties, + Properties, TimedeltaProperties, ) from typing_extensions import assert_type @@ -45,8 +45,9 @@ def test_dt_property() -> None: if TYPE_CHECKING_INVALID_USAGE: s = pd.DataFrame({"a": [1]})["a"] - assert_type(s.dt, Any) - assert_type(s.dt.year, Any) + # python/mypy#19952: mypy believes Properties and its subclasses have a + # conflict and gives Any for s.dt + assert_type(s.dt, Properties) # type: ignore[assert-type] _1 = pd.Series([1]).dt # type: ignore[arg-type] # pyright: ignore[reportAttributeAccessIssue] diff --git a/tests/series/test_series.py b/tests/series/test_series.py index db7e363b1..8b7f77638 100644 --- a/tests/series/test_series.py +++ b/tests/series/test_series.py @@ -3508,8 +3508,25 @@ def test_diff() -> None: pd.Series([True, True, False, False, True]).diff(), "pd.Series[Any]" ), pd.Series, - object, + bool, + index_to_check_for_type=-1, ) + # nullable bool -> nullable bool + with pytest_warns_bounded( + UserWarning, + r"Instantiating BooleanDtype without any arguments.Pass a BooleanDtype instance to silence this warning.", + ): + check( + assert_type( + pd.Series( + [True, True, False, False, True], dtype=pd.BooleanDtype + ).diff(), + "pd.Series[pd.BooleanDtype]", + ), + pd.Series, + np.bool_, + index_to_check_for_type=-1, + ) # Any -> float s_o = s.astype(object) assert_type(s_o, "pd.Series[Any]") @@ -3519,22 +3536,20 @@ def test_diff() -> None: assert_type(s.astype(complex).diff(), "pd.Series[complex]"), pd.Series, complex ) - def _diff_invalid0(): # pyright: ignore[reportUnusedFunction] - # interval -> TypeError: IntervalArray has no 'diff' method. Convert to a suitable dtype prior to calling 'diff'. - assert_type(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff(), Never) - - def _diff_invalid1() -> None: # pyright: ignore[reportUnusedFunction] + if TYPE_CHECKING_INVALID_USAGE: # bytes -> numpy.core._exceptions._UFuncNoLoopError: ufunc 'subtract' did not contain a loop with signature matching types (dtype('S21'), dtype('S21')) -> None pd.Series([1, 1, 2, 3, 5, 8]).astype(bytes).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] - def _diff_invalid2() -> None: # pyright: ignore[reportUnusedFunction] # dtype -> TypeError: unsupported operand type(s) for -: 'type' and 'type' pd.Series([str, int, bool]).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] - def _diff_invalid3() -> None: # pyright: ignore[reportUnusedFunction] # str -> TypeError: unsupported operand type(s) for -: 'str' and 'str' pd.Series(["a", "b"]).diff() # type: ignore[misc] # pyright: ignore[reportAttributeAccessIssue] + def _diff_invalid0(): # pyright: ignore[reportUnusedFunction] + # interval -> TypeError: IntervalArray has no 'diff' method. Convert to a suitable dtype prior to calling 'diff'. + assert_type(pd.Series([pd.Interval(0, 2), pd.Interval(1, 4)]).diff(), Never) + def test_operator_constistency() -> None: # created for #748 From 0a841c77f43c119ee868d0c2e9bebf0afbc4258f Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Tue, 30 Sep 2025 16:47:27 +0200 Subject: [PATCH 13/14] fix: comment --- tests/indexes/test_indexes.py | 11 +++++++++-- tests/series/test_properties.py | 14 ++++++++++++-- tests/series/test_series.py | 25 ++++++++++++------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/tests/indexes/test_indexes.py b/tests/indexes/test_indexes.py index 91ca1b414..5311b085d 100644 --- a/tests/indexes/test_indexes.py +++ b/tests/indexes/test_indexes.py @@ -1524,8 +1524,15 @@ def test_period_index_asof_locs() -> None: def test_array_property() -> None: """Test that Index.array and semantic Index.array return ExtensionArray and its subclasses""" - # pandas-dev/pandas-stubs#1383 - # check(assert_type(Index([1], dtype="category").array, pd.Categorical), pd.Categorical, np.int64) + # casting due to pandas-dev/pandas-stubs#1383 + check( + assert_type( + cast("Index[pd.CategoricalDtype]", Index([1], dtype="category")).array, + pd.Categorical, + ), + pd.Categorical, + np.int64, + ) check( assert_type(pd.interval_range(0, 1).array, IntervalArray), IntervalArray, diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index 9e5229f71..f548f89d8 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -1,5 +1,6 @@ from typing import ( TYPE_CHECKING, + cast, ) import numpy as np @@ -53,8 +54,17 @@ def test_dt_property() -> None: def test_array_property() -> None: """Test that Series.array returns ExtensionArray and its subclasses""" - # pandas-dev/pandas-stubs#1383 - # check(assert_type(pd.Series([1], dtype="category").array, pd.Categorical), pd.Categorical, np.int64) + # casting due to pandas-dev/pandas-stubs#1383 + check( + assert_type( + cast( + "pd.Series[pd.CategoricalDtype]", pd.Series([1], dtype="category") + ).array, + pd.Categorical, + ), + pd.Categorical, + np.int64, + ) check( assert_type(pd.Series(pd.interval_range(0, 1)).array, IntervalArray), IntervalArray, diff --git a/tests/series/test_series.py b/tests/series/test_series.py index d9a58ecda..029471742 100644 --- a/tests/series/test_series.py +++ b/tests/series/test_series.py @@ -21,6 +21,7 @@ TypedDict, TypeVar, Union, + cast, ) import numpy as np @@ -3514,21 +3515,19 @@ def test_diff() -> None: index_to_check_for_type=-1, ) # nullable bool -> nullable bool - with pytest_warns_bounded( - UserWarning, - r"Instantiating BooleanDtype without any arguments.Pass a BooleanDtype instance to silence this warning.", - ): - check( - assert_type( - pd.Series( - [True, True, False, False, True], dtype=pd.BooleanDtype - ).diff(), + # casting due to pandas-dev/pandas-stubs#1395 + check( + assert_type( + cast( "pd.Series[pd.BooleanDtype]", + pd.Series([True, True, False, False, True], dtype="boolean").diff(), ), - pd.Series, - np.bool_, - index_to_check_for_type=-1, - ) + "pd.Series[pd.BooleanDtype]", + ), + pd.Series, + np.bool_, + index_to_check_for_type=-1, + ) # Any -> float s_o = s.astype(object) assert_type(s_o, "pd.Series[Any]") From 984cff587ff1b254147830874b5afe01229fb6ad Mon Sep 17 00:00:00 2001 From: cmp0xff Date: Tue, 30 Sep 2025 16:51:32 +0200 Subject: [PATCH 14/14] fix: pytest --- tests/indexes/test_indexes.py | 2 +- tests/series/test_properties.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/indexes/test_indexes.py b/tests/indexes/test_indexes.py index 5311b085d..1a21d9149 100644 --- a/tests/indexes/test_indexes.py +++ b/tests/indexes/test_indexes.py @@ -1531,7 +1531,7 @@ def test_array_property() -> None: pd.Categorical, ), pd.Categorical, - np.int64, + int, ) check( assert_type(pd.interval_range(0, 1).array, IntervalArray), diff --git a/tests/series/test_properties.py b/tests/series/test_properties.py index f548f89d8..72cf432a6 100644 --- a/tests/series/test_properties.py +++ b/tests/series/test_properties.py @@ -63,7 +63,7 @@ def test_array_property() -> None: pd.Categorical, ), pd.Categorical, - np.int64, + int, ) check( assert_type(pd.Series(pd.interval_range(0, 1)).array, IntervalArray),