diff --git a/pandas-stubs/core/series.pyi b/pandas-stubs/core/series.pyi index 00537b87a..f5bea7bfc 100644 --- a/pandas-stubs/core/series.pyi +++ b/pandas-stubs/core/series.pyi @@ -188,6 +188,7 @@ from pandas._typing import ( np_ndarray_complex, np_ndarray_dt, np_ndarray_float, + np_ndarray_str, np_ndarray_td, npt, num, @@ -1673,7 +1674,8 @@ class Series(IndexOpsMixin[S1], NDFrame): def __add__( self: Series[complex], other: ( - Sequence[_T_COMPLEX] + _T_COMPLEX + | Sequence[_T_COMPLEX] | np_ndarray_bool | np_ndarray_anyint | np_ndarray_float @@ -1682,7 +1684,16 @@ class Series(IndexOpsMixin[S1], NDFrame): ), ) -> Series[complex]: ... @overload - def __add__(self, other: S1 | Series[S1]) -> Self: ... + def __add__( + self: Series[_str], + other: ( + np_ndarray_bool | np_ndarray_anyint | np_ndarray_float | np_ndarray_complex + ), + ) -> Never: ... + @overload + def __add__( + self: Series[_str], other: _str | Sequence[_str] | np_ndarray_str | Series[_str] + ) -> Series[_str]: ... @overload def add( self: Series[Never], @@ -1808,7 +1819,8 @@ class Series(IndexOpsMixin[S1], NDFrame): def add( self: Series[complex], other: ( - Sequence[_T_COMPLEX] + _T_COMPLEX + | Sequence[_T_COMPLEX] | np_ndarray_bool | np_ndarray_anyint | np_ndarray_float @@ -1821,13 +1833,13 @@ class Series(IndexOpsMixin[S1], NDFrame): ) -> Series[complex]: ... @overload def add( - self, - other: S1 | Series[S1], + self: Series[_str], + other: _str | Sequence[_str] | np_ndarray_str | Series[_str], level: Level | None = None, fill_value: float | None = None, axis: int = 0, - ) -> Self: ... - @overload + ) -> Series[_str]: ... + @overload # type: ignore[override] def __radd__(self: Series[Never], other: Scalar | _ListLike) -> Series: ... @overload def __radd__( @@ -1874,7 +1886,8 @@ class Series(IndexOpsMixin[S1], NDFrame): def __radd__( self: Series[complex], other: ( - Sequence[_T_COMPLEX] + _T_COMPLEX + | Sequence[_T_COMPLEX] | np_ndarray_bool | np_ndarray_anyint | np_ndarray_float @@ -1886,7 +1899,16 @@ class Series(IndexOpsMixin[S1], NDFrame): self: Series[_T_COMPLEX], other: np_ndarray_complex ) -> Series[complex]: ... @overload - def __radd__(self, other: S1 | Series[S1]) -> Self: ... + def __radd__( + self: Series[_str], + other: ( + np_ndarray_bool | np_ndarray_anyint | np_ndarray_float | np_ndarray_complex + ), + ) -> Never: ... + @overload + def __radd__( + self: Series[_str], other: _str | Sequence[_str] | np_ndarray_str | Series[_str] + ) -> Series[_str]: ... @overload def radd( self: Series[Never], @@ -1980,7 +2002,8 @@ class Series(IndexOpsMixin[S1], NDFrame): def radd( self: Series[complex], other: ( - Sequence[_T_COMPLEX] + _T_COMPLEX + | Sequence[_T_COMPLEX] | np_ndarray_bool | np_ndarray_anyint | np_ndarray_float @@ -2000,12 +2023,12 @@ class Series(IndexOpsMixin[S1], NDFrame): ) -> Series[complex]: ... @overload def radd( - self, - other: S1 | Series[S1], + self: Series[_str], + other: _str | Sequence[_str] | np_ndarray_str | Series[_str], level: Level | None = None, fill_value: float | None = None, axis: int = 0, - ) -> Self: ... + ) -> Series[_str]: ... # ignore needed for mypy as we want different results based on the arguments @overload # type: ignore[override] def __and__( # pyright: ignore[reportOverlappingOverload] @@ -3682,7 +3705,7 @@ class TimestampSeries(_SeriesSubclassBase[Timestamp, np.datetime64]): @property def dt(self) -> TimestampProperties: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] def __add__(self, other: TimedeltaSeries | np.timedelta64 | timedelta | BaseOffset) -> TimestampSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - def __radd__(self, other: TimedeltaSeries | np.timedelta64 | timedelta) -> TimestampSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + def __radd__(self, other: TimedeltaSeries | np.timedelta64 | timedelta) -> TimestampSeries: ... # type: ignore[override] @overload # type: ignore[override] def __sub__( self, other: Timestamp | datetime | TimestampSeries @@ -3742,7 +3765,7 @@ class TimedeltaSeries(_SeriesSubclassBase[Timedelta, np.timedelta64]): def __add__( # pyright: ignore[reportIncompatibleMethodOverride] self, other: timedelta | Timedelta | np.timedelta64 ) -> TimedeltaSeries: ... - def __radd__(self, other: datetime | Timestamp | TimestampSeries) -> TimestampSeries: ... # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] + def __radd__(self, other: datetime | Timestamp | TimestampSeries) -> TimestampSeries: ... # type: ignore[override] def __mul__( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] self, other: num | Sequence[num] | Series[int] | Series[float] ) -> TimedeltaSeries: ... @@ -3859,9 +3882,7 @@ class OffsetSeries(_SeriesSubclassBase[BaseOffset, np.object_]): @overload # type: ignore[override] def __radd__(self, other: Period) -> PeriodSeries: ... @overload - def __radd__( # pyright: ignore[reportIncompatibleMethodOverride] - self, other: BaseOffset - ) -> OffsetSeries: ... + def __radd__(self, other: BaseOffset) -> OffsetSeries: ... def cumprod( self, axis: AxisIndex = ..., diff --git a/pyproject.toml b/pyproject.toml index 0de7fe6ce..046abe579 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ mypy = "1.17.0" pandas = "2.3.1" pyarrow = ">=10.0.1" pytest = ">=7.1.2" -pyright = ">=1.1.403" +pyright = ">=1.1.404" ty = "^0.0.1a8" pyrefly = "^0.21.0" poethepoet = ">=0.16.5" diff --git a/tests/series/arithmetic/str/__init__.py b/tests/series/arithmetic/str/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/series/arithmetic/str/test_add.py b/tests/series/arithmetic/str/test_add.py new file mode 100644 index 000000000..2eaffd59a --- /dev/null +++ b/tests/series/arithmetic/str/test_add.py @@ -0,0 +1,122 @@ +import sys +from typing import Any + +import numpy as np +from numpy import typing as npt # noqa: F401 +import pandas as pd +from typing_extensions import ( + Never, + assert_type, +) + +from tests import ( + TYPE_CHECKING_INVALID_USAGE, + check, +) + +left = pd.Series(["1", "23", "456"]) # left operand + + +def test_add_py_scalar() -> None: + """Testpd.Series[str]+ Python native 'scalar's""" + i = 4 + r0 = "right" + + if TYPE_CHECKING_INVALID_USAGE: + _0 = left + i # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(left + r0, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + _1 = i + left # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(r0 + left, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.add(i) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.radd(i) # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str) + + +def test_add_py_sequence() -> None: + """Testpd.Series[str]+ Python native sequence""" + i = [3, 5, 8] + r0 = ["a", "bc", "def"] + r1 = tuple(r0) + + if TYPE_CHECKING_INVALID_USAGE: + _0 = left + i # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(left + r0, "pd.Series[str]"), pd.Series, str) + check(assert_type(left + r1, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + _1 = i + left # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(r0 + left, "pd.Series[str]"), pd.Series, str) + check(assert_type(r1 + left, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.add(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType,reportCallIssue] + check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str) + check(assert_type(left.add(r1), "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str) + check(assert_type(left.radd(r1), "pd.Series[str]"), pd.Series, str) + + +def test_add_numpy_array() -> None: + """Testpd.Series[str]+ numpy array""" + i = np.array([3, 5, 8], np.int64) + r0 = np.array(["a", "bc", "def"], np.str_) + + if TYPE_CHECKING_INVALID_USAGE: + assert_type(left + i, Never) + check(assert_type(left + r0, "pd.Series[str]"), pd.Series, str) + + # `numpy` typing gives `npt.NDArray[np.int64]` in the static type + # checking, where our `__radd__` cannot override. At runtime, they return + # `Series`s. + if TYPE_CHECKING_INVALID_USAGE: + assert_type(i + left, "npt.NDArray[np.int64]") + if sys.version_info >= (3, 11): + # `numpy` typing gives `npt.NDArray[np.int64]` in the static type + # checking, where our `__radd__` cannot override. At runtime, they return + # `Series`s. + check(assert_type(r0 + left, "npt.NDArray[np.str_]"), pd.Series, str) + else: + # Python 3.10 uses NumPy 2.2.6, and it has for r0 ndarray[tuple[int,...], dtype[str_]] + # Python 3.11+ uses NumPy 2.3.2, and it has for r0 ndarray[tuple[Any,...,dtype[str_]] + # https://github.com/pandas-dev/pandas-stubs/pull/1274#discussion_r2291498975 + check(assert_type(r0 + left, Any), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.add(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType,reportCallIssue] + check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str) + + +def test_add_pd_series() -> None: + """Testpd.Series[str]+ pandas series""" + i = pd.Series([3, 5, 8]) + r0 = pd.Series(["a", "bc", "def"]) + + if TYPE_CHECKING_INVALID_USAGE: + _0 = left + i # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(left + r0, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + _1 = i + left # type: ignore[operator] # pyright: ignore[reportOperatorIssue] + check(assert_type(r0 + left, "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.add(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType,reportCallIssue] + check(assert_type(left.add(r0), "pd.Series[str]"), pd.Series, str) + + if TYPE_CHECKING_INVALID_USAGE: + left.radd(i) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + check(assert_type(left.radd(r0), "pd.Series[str]"), pd.Series, str)