From f26031e8c076760afbc163a21ce45bfd4720cff0 Mon Sep 17 00:00:00 2001 From: Terji Petersen Date: Wed, 15 Mar 2023 13:54:07 +0000 Subject: [PATCH] TYP: Use Self instead of class-bound TypeVar (generic.py/frame.py/series.py) (#51493) * TYP: Use Self for type checking * type frame.py & series.py * TYP: change Index to Self --- pandas/core/frame.py | 7 +- pandas/core/generic.py | 333 ++++++++++++++-------------- pandas/core/indexes/base.py | 67 +++--- pandas/core/indexes/datetimelike.py | 17 +- pandas/core/indexes/extension.py | 1 - pandas/core/series.py | 5 +- 6 files changed, 206 insertions(+), 224 deletions(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index ce4c3d81c4f90..fa2821a6335a3 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -231,6 +231,7 @@ ReadBuffer, Renamer, Scalar, + Self, SortKind, StorageOptions, Suffixes, @@ -2495,7 +2496,7 @@ def _from_arrays( index, dtype: Dtype | None = None, verify_integrity: bool = True, - ) -> DataFrame: + ) -> Self: """ Create DataFrame from a list of arrays corresponding to the columns. @@ -4597,7 +4598,7 @@ def eval(self, expr: str, *, inplace: bool = False, **kwargs) -> Any | None: return _eval(expr, inplace=inplace, **kwargs) - def select_dtypes(self, include=None, exclude=None) -> DataFrame: + def select_dtypes(self, include=None, exclude=None) -> Self: """ Return a subset of the DataFrame's columns based on the column dtypes. @@ -5015,7 +5016,7 @@ def align( limit: int | None = None, fill_axis: Axis = 0, broadcast_axis: Axis | None = None, - ) -> tuple[DataFrame, NDFrameT]: + ) -> tuple[Self, NDFrameT]: return super().align( other, join=join, diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 17e4a4c142f66..000b0c5d1ff21 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -72,6 +72,7 @@ RandomState, Renamer, Scalar, + Self, SortKind, StorageOptions, Suffixes, @@ -311,7 +312,7 @@ def _init_mgr( mgr = mgr.astype(dtype=dtype) return mgr - def _as_manager(self: NDFrameT, typ: str, copy: bool_t = True) -> NDFrameT: + def _as_manager(self, typ: str, copy: bool_t = True) -> Self: """ Private helper function to create a DataFrame with specific manager. @@ -400,11 +401,11 @@ def flags(self) -> Flags: @final def set_flags( - self: NDFrameT, + self, *, copy: bool_t = False, allows_duplicate_labels: bool_t | None = None, - ) -> NDFrameT: + ) -> Self: """ Return a new object with updated flags. @@ -471,7 +472,7 @@ def _validate_dtype(cls, dtype) -> DtypeObj | None: # Construction @property - def _constructor(self: NDFrameT) -> Callable[..., NDFrameT]: + def _constructor(self) -> Callable[..., Self]: """ Used when a manipulation result has the same dimensions as the original. @@ -671,12 +672,12 @@ def size(self) -> int: return np.prod(self.shape) # type: ignore[return-value] def set_axis( - self: NDFrameT, + self, labels, *, axis: Axis = 0, copy: bool_t | None = None, - ) -> NDFrameT: + ) -> Self: """ Assign desired index to given axis. @@ -732,9 +733,7 @@ def _set_axis(self, axis: AxisInt, labels: AnyArrayLike | list) -> None: self._clear_item_cache() @final - def swapaxes( - self: NDFrameT, axis1: Axis, axis2: Axis, copy: bool_t | None = None - ) -> NDFrameT: + def swapaxes(self, axis1: Axis, axis2: Axis, copy: bool_t | None = None) -> Self: """ Interchange axes and swap values axes appropriately. @@ -785,7 +784,7 @@ def swapaxes( @final @doc(klass=_shared_doc_kwargs["klass"]) - def droplevel(self: NDFrameT, level: IndexLabel, axis: Axis = 0) -> NDFrameT: + def droplevel(self, level: IndexLabel, axis: Axis = 0) -> Self: """ Return {klass} with requested index / column level(s) removed. @@ -970,7 +969,7 @@ def squeeze(self, axis: Axis | None = None): # Rename def _rename( - self: NDFrameT, + self, mapper: Renamer | None = None, *, index: Renamer | None = None, @@ -980,7 +979,7 @@ def _rename( inplace: bool_t = False, level: Level | None = None, errors: str = "ignore", - ) -> NDFrameT | None: + ) -> Self | None: # called by Series.rename and DataFrame.rename if mapper is None and index is None and columns is None: @@ -1042,7 +1041,7 @@ def _rename( @overload def rename_axis( - self: NDFrameT, + self, mapper: IndexLabel | lib.NoDefault = ..., *, index=..., @@ -1050,7 +1049,7 @@ def rename_axis( axis: Axis = ..., copy: bool_t | None = ..., inplace: Literal[False] = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -1068,7 +1067,7 @@ def rename_axis( @overload def rename_axis( - self: NDFrameT, + self, mapper: IndexLabel | lib.NoDefault = ..., *, index=..., @@ -1076,11 +1075,11 @@ def rename_axis( axis: Axis = ..., copy: bool_t | None = ..., inplace: bool_t = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... def rename_axis( - self: NDFrameT, + self, mapper: IndexLabel | lib.NoDefault = lib.no_default, *, index=lib.no_default, @@ -1088,7 +1087,7 @@ def rename_axis( axis: Axis = 0, copy: bool_t | None = None, inplace: bool_t = False, - ) -> NDFrameT | None: + ) -> Self | None: """ Set the name of the axis for the index or columns. @@ -1419,7 +1418,7 @@ def equals(self, other: object) -> bool_t: # Unary Methods @final - def __neg__(self: NDFrameT) -> NDFrameT: + def __neg__(self) -> Self: def blk_func(values: ArrayLike): if is_bool_dtype(values.dtype): # error: Argument 1 to "inv" has incompatible type "Union @@ -1437,7 +1436,7 @@ def blk_func(values: ArrayLike): return res.__finalize__(self, method="__neg__") @final - def __pos__(self: NDFrameT) -> NDFrameT: + def __pos__(self) -> Self: def blk_func(values: ArrayLike): if is_bool_dtype(values.dtype): return values.copy() @@ -1452,7 +1451,7 @@ def blk_func(values: ArrayLike): return res.__finalize__(self, method="__pos__") @final - def __invert__(self: NDFrameT) -> NDFrameT: + def __invert__(self) -> Self: if not self.size: # inv fails with 0 len return self.copy(deep=False) @@ -1517,7 +1516,7 @@ def bool(self) -> bool_t: return True @final - def abs(self: NDFrameT) -> NDFrameT: + def abs(self) -> Self: """ Return a Series/DataFrame with absolute numeric value of each element. @@ -1588,11 +1587,11 @@ def abs(self: NDFrameT) -> NDFrameT: return self._constructor(res_mgr).__finalize__(self, name="abs") @final - def __abs__(self: NDFrameT) -> NDFrameT: + def __abs__(self) -> Self: return self.abs() @final - def __round__(self: NDFrameT, decimals: int = 0) -> NDFrameT: + def __round__(self, decimals: int = 0) -> Self: return self.round(decimals).__finalize__(self, method="__round__") # ------------------------------------------------------------------------- @@ -3822,7 +3821,7 @@ def _clear_item_cache(self) -> None: # Indexing Methods @final - def take(self: NDFrameT, indices, axis: Axis = 0, **kwargs) -> NDFrameT: + def take(self, indices, axis: Axis = 0, **kwargs) -> Self: """ Return the elements in the given *positional* indices along an axis. @@ -3936,7 +3935,7 @@ class max_speed return self._constructor(new_data).__finalize__(self, method="take") @final - def _take_with_is_copy(self: NDFrameT, indices, axis: Axis = 0) -> NDFrameT: + def _take_with_is_copy(self, indices, axis: Axis = 0) -> Self: """ Internal version of the `take` method that sets the `_is_copy` attribute to keep track of the parent dataframe (using in indexing @@ -3954,12 +3953,12 @@ def _take_with_is_copy(self: NDFrameT, indices, axis: Axis = 0) -> NDFrameT: @final def xs( - self: NDFrameT, + self, key: IndexLabel, axis: Axis = 0, level: IndexLabel = None, drop_level: bool_t = True, - ) -> NDFrameT: + ) -> Self: """ Return cross-section from the Series/DataFrame. @@ -4134,7 +4133,7 @@ class animal locomotion def __getitem__(self, item): raise AbstractMethodError(self) - def _slice(self: NDFrameT, slobj: slice, axis: Axis = 0) -> NDFrameT: + def _slice(self, slobj: slice, axis: Axis = 0) -> Self: """ Construct a slice of this container. @@ -4364,13 +4363,13 @@ def _is_view(self) -> bool_t: @final def reindex_like( - self: NDFrameT, + self, other, method: Literal["backfill", "bfill", "pad", "ffill", "nearest"] | None = None, copy: bool_t | None = None, limit=None, tolerance=None, - ) -> NDFrameT: + ) -> Self: """ Return an object with matching indices as other object. @@ -4490,7 +4489,7 @@ def drop( @overload def drop( - self: NDFrameT, + self, labels: IndexLabel = ..., *, axis: Axis = ..., @@ -4499,12 +4498,12 @@ def drop( level: Level | None = ..., inplace: Literal[False] = ..., errors: IgnoreRaise = ..., - ) -> NDFrameT: + ) -> Self: ... @overload def drop( - self: NDFrameT, + self, labels: IndexLabel = ..., *, axis: Axis = ..., @@ -4513,11 +4512,11 @@ def drop( level: Level | None = ..., inplace: bool_t = ..., errors: IgnoreRaise = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... def drop( - self: NDFrameT, + self, labels: IndexLabel = None, *, axis: Axis = 0, @@ -4526,7 +4525,7 @@ def drop( level: Level | None = None, inplace: bool_t = False, errors: IgnoreRaise = "raise", - ) -> NDFrameT | None: + ) -> Self | None: inplace = validate_bool_kwarg(inplace, "inplace") if labels is not None: @@ -4557,13 +4556,13 @@ def drop( @final def _drop_axis( - self: NDFrameT, + self, labels, axis, level=None, errors: IgnoreRaise = "raise", only_slice: bool_t = False, - ) -> NDFrameT: + ) -> Self: """ Drop labels from specified axis. Used in the ``drop`` method internally. @@ -4661,7 +4660,7 @@ def _update_inplace(self, result, verify_is_copy: bool_t = True) -> None: self._maybe_update_cacher(verify_is_copy=verify_is_copy, inplace=True) @final - def add_prefix(self: NDFrameT, prefix: str, axis: Axis | None = None) -> NDFrameT: + def add_prefix(self, prefix: str, axis: Axis | None = None) -> Self: """ Prefix labels with string `prefix`. @@ -4727,15 +4726,15 @@ def add_prefix(self: NDFrameT, prefix: str, axis: Axis | None = None) -> NDFrame mapper = {axis_name: f} - # error: Incompatible return value type (got "Optional[NDFrameT]", - # expected "NDFrameT") + # error: Incompatible return value type (got "Optional[Self]", + # expected "Self") # error: Argument 1 to "rename" of "NDFrame" has incompatible type # "**Dict[str, partial[str]]"; expected "Union[str, int, None]" # error: Keywords must be strings return self._rename(**mapper) # type: ignore[return-value, arg-type, misc] @final - def add_suffix(self: NDFrameT, suffix: str, axis: Axis | None = None) -> NDFrameT: + def add_suffix(self, suffix: str, axis: Axis | None = None) -> Self: """ Suffix labels with string `suffix`. @@ -4800,8 +4799,8 @@ def add_suffix(self: NDFrameT, suffix: str, axis: Axis | None = None) -> NDFrame axis_name = self._get_axis_name(axis) mapper = {axis_name: f} - # error: Incompatible return value type (got "Optional[NDFrameT]", - # expected "NDFrameT") + # error: Incompatible return value type (got "Optional[Self]", + # expected "Self") # error: Argument 1 to "rename" of "NDFrame" has incompatible type # "**Dict[str, partial[str]]"; expected "Union[str, int, None]" # error: Keywords must be strings @@ -4809,7 +4808,7 @@ def add_suffix(self: NDFrameT, suffix: str, axis: Axis | None = None) -> NDFrame @overload def sort_values( - self: NDFrameT, + self, *, axis: Axis = ..., ascending: bool_t | Sequence[bool_t] = ..., @@ -4818,7 +4817,7 @@ def sort_values( na_position: str = ..., ignore_index: bool_t = ..., key: ValueKeyFunc = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -4837,7 +4836,7 @@ def sort_values( @overload def sort_values( - self: NDFrameT, + self, *, axis: Axis = ..., ascending: bool_t | Sequence[bool_t] = ..., @@ -4846,11 +4845,11 @@ def sort_values( na_position: str = ..., ignore_index: bool_t = ..., key: ValueKeyFunc = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... def sort_values( - self: NDFrameT, + self, *, axis: Axis = 0, ascending: bool_t | Sequence[bool_t] = True, @@ -4859,7 +4858,7 @@ def sort_values( na_position: str = "last", ignore_index: bool_t = False, key: ValueKeyFunc = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Sort by the values along either axis. @@ -5021,7 +5020,7 @@ def sort_index( @overload def sort_index( - self: NDFrameT, + self, *, axis: Axis = ..., level: IndexLabel = ..., @@ -5032,12 +5031,12 @@ def sort_index( sort_remaining: bool_t = ..., ignore_index: bool_t = ..., key: IndexKeyFunc = ..., - ) -> NDFrameT: + ) -> Self: ... @overload def sort_index( - self: NDFrameT, + self, *, axis: Axis = ..., level: IndexLabel = ..., @@ -5048,11 +5047,11 @@ def sort_index( sort_remaining: bool_t = ..., ignore_index: bool_t = ..., key: IndexKeyFunc = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... def sort_index( - self: NDFrameT, + self, *, axis: Axis = 0, level: IndexLabel = None, @@ -5063,7 +5062,7 @@ def sort_index( sort_remaining: bool_t = True, ignore_index: bool_t = False, key: IndexKeyFunc = None, - ) -> NDFrameT | None: + ) -> Self | None: inplace = validate_bool_kwarg(inplace, "inplace") axis = self._get_axis_number(axis) ascending = validate_ascending(ascending) @@ -5109,7 +5108,7 @@ def sort_index( optional_reindex="", ) def reindex( - self: NDFrameT, + self, labels=None, index=None, columns=None, @@ -5120,7 +5119,7 @@ def reindex( fill_value: Scalar | None = np.nan, limit: int | None = None, tolerance=None, - ) -> NDFrameT: + ) -> Self: """ Conform {klass} to new index with optional filling logic. @@ -5363,8 +5362,8 @@ def reindex( ).__finalize__(self, method="reindex") def _reindex_axes( - self: NDFrameT, axes, level, limit, tolerance, method, fill_value, copy - ) -> NDFrameT: + self, axes, level, limit, tolerance, method, fill_value, copy + ) -> Self: """Perform the reindex for all the axes.""" obj = self for a in self._AXIS_ORDERS: @@ -5408,12 +5407,12 @@ def _reindex_multi(self, axes, copy, fill_value): @final def _reindex_with_indexers( - self: NDFrameT, + self, reindexers, fill_value=None, copy: bool_t | None = False, allow_dups: bool_t = False, - ) -> NDFrameT: + ) -> Self: """allow_dups indicates an internal call here""" # reindex doing multiple operations on different axes if indicated new_data = self._mgr @@ -5452,12 +5451,12 @@ def _reindex_with_indexers( return self._constructor(new_data).__finalize__(self) def filter( - self: NDFrameT, + self, items=None, like: str | None = None, regex: str | None = None, axis: Axis | None = None, - ) -> NDFrameT: + ) -> Self: """ Subset the dataframe rows or columns according to the specified index labels. @@ -5558,7 +5557,7 @@ def f(x) -> bool_t: raise TypeError("Must pass either `items`, `like`, or `regex`") @final - def head(self: NDFrameT, n: int = 5) -> NDFrameT: + def head(self, n: int = 5) -> Self: """ Return the first `n` rows. @@ -5633,7 +5632,7 @@ def head(self: NDFrameT, n: int = 5) -> NDFrameT: return self.iloc[:n] @final - def tail(self: NDFrameT, n: int = 5) -> NDFrameT: + def tail(self, n: int = 5) -> Self: """ Return the last `n` rows. @@ -5711,7 +5710,7 @@ def tail(self: NDFrameT, n: int = 5) -> NDFrameT: @final def sample( - self: NDFrameT, + self, n: int | None = None, frac: float | None = None, replace: bool_t = False, @@ -5719,7 +5718,7 @@ def sample( random_state: RandomState | None = None, axis: Axis | None = None, ignore_index: bool_t = False, - ) -> NDFrameT: + ) -> Self: """ Return a random sample of items from an axis of object. @@ -5975,9 +5974,7 @@ def pipe( # Attribute access @final - def __finalize__( - self: NDFrameT, other, method: str | None = None, **kwargs - ) -> NDFrameT: + def __finalize__(self, other, method: str | None = None, **kwargs) -> Self: """ Propagate metadata from other to self. @@ -6154,7 +6151,7 @@ def _check_inplace_setting(self, value) -> bool_t: return True @final - def _get_numeric_data(self: NDFrameT) -> NDFrameT: + def _get_numeric_data(self) -> Self: return self._constructor(self._mgr.get_numeric_data()).__finalize__(self) @final @@ -6205,8 +6202,8 @@ def dtypes(self): return self._constructor_sliced(data, index=self._info_axis, dtype=np.object_) def astype( - self: NDFrameT, dtype, copy: bool_t | None = None, errors: IgnoreRaise = "raise" - ) -> NDFrameT: + self, dtype, copy: bool_t | None = None, errors: IgnoreRaise = "raise" + ) -> Self: """ Cast a pandas object to a specified dtype ``dtype``. @@ -6378,15 +6375,15 @@ def astype( result = concat(results, axis=1, copy=False) # GH#40810 retain subclass # error: Incompatible types in assignment - # (expression has type "NDFrameT", variable has type "DataFrame") + # (expression has type "Self", variable has type "DataFrame") result = self._constructor(result) # type: ignore[assignment] result.columns = self.columns result = result.__finalize__(self, method="astype") # https://github.com/python/mypy/issues/8354 - return cast(NDFrameT, result) + return cast(Self, result) @final - def copy(self: NDFrameT, deep: bool_t | None = True) -> NDFrameT: + def copy(self, deep: bool_t | None = True) -> Self: """ Make a copy of this object's indices and data. @@ -6500,11 +6497,11 @@ def copy(self: NDFrameT, deep: bool_t | None = True) -> NDFrameT: return self._constructor(data).__finalize__(self, method="copy") @final - def __copy__(self: NDFrameT, deep: bool_t = True) -> NDFrameT: + def __copy__(self, deep: bool_t = True) -> Self: return self.copy(deep=deep) @final - def __deepcopy__(self: NDFrameT, memo=None) -> NDFrameT: + def __deepcopy__(self, memo=None) -> Self: """ Parameters ---------- @@ -6514,7 +6511,7 @@ def __deepcopy__(self: NDFrameT, memo=None) -> NDFrameT: return self.copy(deep=True) @final - def infer_objects(self: NDFrameT, copy: bool_t | None = None) -> NDFrameT: + def infer_objects(self, copy: bool_t | None = None) -> Self: """ Attempt to infer better dtypes for object columns. @@ -6563,14 +6560,14 @@ def infer_objects(self: NDFrameT, copy: bool_t | None = None) -> NDFrameT: @final def convert_dtypes( - self: NDFrameT, + self, infer_objects: bool_t = True, convert_string: bool_t = True, convert_integer: bool_t = True, convert_boolean: bool_t = True, convert_floating: bool_t = True, dtype_backend: DtypeBackend = "numpy_nullable", - ) -> NDFrameT: + ) -> Self: """ Convert columns to the best possible dtypes using dtypes supporting ``pd.NA``. @@ -6739,7 +6736,7 @@ def convert_dtypes( result = cons(result) result = result.__finalize__(self, method="convert_dtypes") # https://github.com/python/mypy/issues/8354 - return cast(NDFrameT, result) + return cast(Self, result) else: return self.copy(deep=None) @@ -6748,7 +6745,7 @@ def convert_dtypes( @overload def fillna( - self: NDFrameT, + self, value: Hashable | Mapping | Series | DataFrame = ..., *, method: FillnaOptions | None = ..., @@ -6756,7 +6753,7 @@ def fillna( inplace: Literal[False] = ..., limit: int | None = ..., downcast: dict | None = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -6774,7 +6771,7 @@ def fillna( @overload def fillna( - self: NDFrameT, + self, value: Hashable | Mapping | Series | DataFrame = ..., *, method: FillnaOptions | None = ..., @@ -6782,12 +6779,12 @@ def fillna( inplace: bool_t = ..., limit: int | None = ..., downcast: dict | None = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc(**_shared_doc_kwargs) def fillna( - self: NDFrameT, + self, value: Hashable | Mapping | Series | DataFrame = None, *, method: FillnaOptions | None = None, @@ -6795,7 +6792,7 @@ def fillna( inplace: bool_t = False, limit: int | None = None, downcast: dict | None = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Fill NA/NaN values using the specified method. @@ -7049,13 +7046,13 @@ def fillna( @overload def ffill( - self: NDFrameT, + self, *, axis: None | Axis = ..., inplace: Literal[False] = ..., limit: None | int = ..., downcast: dict | None = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -7071,24 +7068,24 @@ def ffill( @overload def ffill( - self: NDFrameT, + self, *, axis: None | Axis = ..., inplace: bool_t = ..., limit: None | int = ..., downcast: dict | None = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc(klass=_shared_doc_kwargs["klass"]) def ffill( - self: NDFrameT, + self, *, axis: None | Axis = None, inplace: bool_t = False, limit: None | int = None, downcast: dict | None = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Synonym for :meth:`DataFrame.fillna` with ``method='ffill'``. @@ -7103,13 +7100,13 @@ def ffill( @doc(klass=_shared_doc_kwargs["klass"]) def pad( - self: NDFrameT, + self, *, axis: None | Axis = None, inplace: bool_t = False, limit: None | int = None, downcast: dict | None = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Synonym for :meth:`DataFrame.fillna` with ``method='ffill'``. @@ -7132,13 +7129,13 @@ def pad( @overload def bfill( - self: NDFrameT, + self, *, axis: None | Axis = ..., inplace: Literal[False] = ..., limit: None | int = ..., downcast: dict | None = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -7154,24 +7151,24 @@ def bfill( @overload def bfill( - self: NDFrameT, + self, *, axis: None | Axis = ..., inplace: bool_t = ..., limit: None | int = ..., downcast: dict | None = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc(klass=_shared_doc_kwargs["klass"]) def bfill( - self: NDFrameT, + self, *, axis: None | Axis = None, inplace: bool_t = False, limit: None | int = None, downcast: dict | None = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Synonym for :meth:`DataFrame.fillna` with ``method='bfill'``. @@ -7186,13 +7183,13 @@ def bfill( @doc(klass=_shared_doc_kwargs["klass"]) def backfill( - self: NDFrameT, + self, *, axis: None | Axis = None, inplace: bool_t = False, limit: None | int = None, downcast: dict | None = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Synonym for :meth:`DataFrame.fillna` with ``method='bfill'``. @@ -7215,7 +7212,7 @@ def backfill( @overload def replace( - self: NDFrameT, + self, to_replace=..., value=..., *, @@ -7223,7 +7220,7 @@ def replace( limit: int | None = ..., regex: bool_t = ..., method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -7241,7 +7238,7 @@ def replace( @overload def replace( - self: NDFrameT, + self, to_replace=..., value=..., *, @@ -7249,7 +7246,7 @@ def replace( limit: int | None = ..., regex: bool_t = ..., method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc( @@ -7259,7 +7256,7 @@ def replace( replace_iloc=_shared_doc_kwargs["replace_iloc"], ) def replace( - self: NDFrameT, + self, to_replace=None, value=lib.no_default, *, @@ -7267,7 +7264,7 @@ def replace( limit: int | None = None, regex: bool_t = False, method: Literal["pad", "ffill", "bfill"] | lib.NoDefault = lib.no_default, - ) -> NDFrameT | None: + ) -> Self | None: if not ( is_scalar(to_replace) or is_re_compilable(to_replace) @@ -7454,7 +7451,7 @@ def replace( return result.__finalize__(self, method="replace") def interpolate( - self: NDFrameT, + self, method: str = "linear", *, axis: Axis = 0, @@ -7464,7 +7461,7 @@ def interpolate( limit_area: str | None = None, downcast: str | None = None, **kwargs, - ) -> NDFrameT | None: + ) -> Self | None: """ Fill NaN values using an interpolation method. @@ -7929,7 +7926,7 @@ def asof(self, where, subset=None): # Action Methods @doc(klass=_shared_doc_kwargs["klass"]) - def isna(self: NDFrameT) -> NDFrameT: + def isna(self) -> Self: """ Detect missing values. @@ -7992,11 +7989,11 @@ def isna(self: NDFrameT) -> NDFrameT: return isna(self).__finalize__(self, method="isna") @doc(isna, klass=_shared_doc_kwargs["klass"]) - def isnull(self: NDFrameT) -> NDFrameT: + def isnull(self) -> Self: return isna(self).__finalize__(self, method="isnull") @doc(klass=_shared_doc_kwargs["klass"]) - def notna(self: NDFrameT) -> NDFrameT: + def notna(self) -> Self: """ Detect existing (non-missing) values. @@ -8059,7 +8056,7 @@ def notna(self: NDFrameT) -> NDFrameT: return notna(self).__finalize__(self, method="notna") @doc(notna, klass=_shared_doc_kwargs["klass"]) - def notnull(self: NDFrameT) -> NDFrameT: + def notnull(self) -> Self: return notna(self).__finalize__(self, method="notnull") @final @@ -8120,14 +8117,14 @@ def _clip_with_one_bound(self, threshold, method, axis, inplace): return self.where(subset, threshold, axis=axis, inplace=inplace) def clip( - self: NDFrameT, + self, lower=None, upper=None, *, axis: Axis | None = None, inplace: bool_t = False, **kwargs, - ) -> NDFrameT | None: + ) -> Self | None: """ Trim values at input threshold(s). @@ -8281,13 +8278,13 @@ def clip( @doc(**_shared_doc_kwargs) def asfreq( - self: NDFrameT, + self, freq: Frequency, method: FillnaOptions | None = None, how: str | None = None, normalize: bool_t = False, fill_value: Hashable = None, - ) -> NDFrameT: + ) -> Self: """ Convert time series to specified frequency. @@ -8405,9 +8402,7 @@ def asfreq( ) @final - def at_time( - self: NDFrameT, time, asof: bool_t = False, axis: Axis | None = None - ) -> NDFrameT: + def at_time(self, time, asof: bool_t = False, axis: Axis | None = None) -> Self: """ Select values at particular time of day (e.g., 9:30AM). @@ -8465,12 +8460,12 @@ def at_time( @final def between_time( - self: NDFrameT, + self, start_time, end_time, inclusive: IntervalClosedType = "both", axis: Axis | None = None, - ) -> NDFrameT: + ) -> Self: """ Select values between particular times of the day (e.g., 9:00-9:30 AM). @@ -8949,7 +8944,7 @@ def resample( ) @final - def first(self: NDFrameT, offset) -> NDFrameT: + def first(self, offset) -> Self: """ Select initial periods of time series data based on a date offset. @@ -9022,7 +9017,7 @@ def first(self: NDFrameT, offset) -> NDFrameT: return self.loc[:end] @final - def last(self: NDFrameT, offset) -> NDFrameT: + def last(self, offset) -> Self: """ Select final periods of time series data based on a date offset. @@ -9087,14 +9082,14 @@ def last(self: NDFrameT, offset) -> NDFrameT: @final def rank( - self: NDFrameT, + self, axis: Axis = 0, method: str = "average", numeric_only: bool_t = False, na_option: str = "keep", ascending: bool_t = True, pct: bool_t = False, - ) -> NDFrameT: + ) -> Self: """ Compute numerical data ranks (1 through n) along axis. @@ -9317,8 +9312,8 @@ def compare( @doc(**_shared_doc_kwargs) def align( - self: NDFrameT, - other: NDFrameTb, + self, + other: NDFrameT, join: AlignJoin = "outer", axis: Axis | None = None, level: Level = None, @@ -9328,7 +9323,7 @@ def align( limit: int | None = None, fill_axis: Axis = 0, broadcast_axis: Axis | None = None, - ) -> tuple[NDFrameT, NDFrameTb]: + ) -> tuple[Self, NDFrameT]: """ Align two objects on their axes with the specified join method. @@ -9802,14 +9797,14 @@ def _where( @overload def where( - self: NDFrameT, + self, cond, other=..., *, inplace: Literal[False] = ..., axis: Axis | None = ..., level: Level = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -9826,14 +9821,14 @@ def where( @overload def where( - self: NDFrameT, + self, cond, other=..., *, inplace: bool_t = ..., axis: Axis | None = ..., level: Level = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc( @@ -9844,14 +9839,14 @@ def where( name_other="mask", ) def where( - self: NDFrameT, + self, cond, other=np.nan, *, inplace: bool_t = False, axis: Axis | None = None, level: Level = None, - ) -> NDFrameT | None: + ) -> Self | None: """ Replace values where the condition is {cond_rev}. @@ -9994,14 +9989,14 @@ def where( @overload def mask( - self: NDFrameT, + self, cond, other=..., *, inplace: Literal[False] = ..., axis: Axis | None = ..., level: Level = ..., - ) -> NDFrameT: + ) -> Self: ... @overload @@ -10018,14 +10013,14 @@ def mask( @overload def mask( - self: NDFrameT, + self, cond, other=..., *, inplace: bool_t = ..., axis: Axis | None = ..., level: Level = ..., - ) -> NDFrameT | None: + ) -> Self | None: ... @doc( @@ -10037,14 +10032,14 @@ def mask( name_other="where", ) def mask( - self: NDFrameT, + self, cond, other=lib.no_default, *, inplace: bool_t = False, axis: Axis | None = None, level: Level = None, - ) -> NDFrameT | None: + ) -> Self | None: inplace = validate_bool_kwarg(inplace, "inplace") cond = common.apply_if_callable(cond, self) @@ -10062,12 +10057,12 @@ def mask( @doc(klass=_shared_doc_kwargs["klass"]) def shift( - self: NDFrameT, + self, periods: int = 1, freq=None, axis: Axis = 0, fill_value: Hashable = None, - ) -> NDFrameT: + ) -> Self: """ Shift index by desired number of periods with an optional time `freq`. @@ -10209,12 +10204,12 @@ def shift( return result.__finalize__(self, method="shift") def truncate( - self: NDFrameT, + self, before=None, after=None, axis: Axis | None = None, copy: bool_t | None = None, - ) -> NDFrameT: + ) -> Self: """ Truncate a Series or DataFrame before and after some index value. @@ -10371,8 +10366,8 @@ def truncate( @final @doc(klass=_shared_doc_kwargs["klass"]) def tz_convert( - self: NDFrameT, tz, axis: Axis = 0, level=None, copy: bool_t | None = None - ) -> NDFrameT: + self, tz, axis: Axis = 0, level=None, copy: bool_t | None = None + ) -> Self: """ Convert tz-aware axis to target time zone. @@ -10452,14 +10447,14 @@ def _tz_convert(ax, tz): @final @doc(klass=_shared_doc_kwargs["klass"]) def tz_localize( - self: NDFrameT, + self, tz, axis: Axis = 0, level=None, copy: bool_t | None = None, ambiguous: TimeAmbiguous = "raise", nonexistent: TimeNonexistent = "raise", - ) -> NDFrameT: + ) -> Self: """ Localize tz-naive index of a Series or DataFrame to target time zone. @@ -10636,11 +10631,11 @@ def _tz_localize(ax, tz, ambiguous, nonexistent): @final def describe( - self: NDFrameT, + self, percentiles=None, include=None, exclude=None, - ) -> NDFrameT: + ) -> Self: """ Generate descriptive statistics. @@ -10885,13 +10880,13 @@ def describe( @final def pct_change( - self: NDFrameT, + self, periods: int = 1, fill_method: Literal["backfill", "bfill", "pad", "ffill"] | None = "pad", limit=None, freq=None, **kwargs, - ) -> NDFrameT: + ) -> Self: """ Percentage change between the current and a prior element. @@ -11015,7 +11010,7 @@ def pct_change( data = _data shifted = data.shift(periods=periods, freq=freq, axis=axis, **kwargs) - # Unsupported left operand type for / ("NDFrameT") + # Unsupported left operand type for / ("Self") rs = data / shifted - 1 # type: ignore[operator] if freq is not None: # Shift method is implemented differently when freq is not None @@ -12028,47 +12023,47 @@ def _inplace_method(self, other, op): ) return self - def __iadd__(self: NDFrameT, other) -> NDFrameT: + def __iadd__(self, other) -> Self: # error: Unsupported left operand type for + ("Type[NDFrame]") return self._inplace_method(other, type(self).__add__) # type: ignore[operator] - def __isub__(self: NDFrameT, other) -> NDFrameT: + def __isub__(self, other) -> Self: # error: Unsupported left operand type for - ("Type[NDFrame]") return self._inplace_method(other, type(self).__sub__) # type: ignore[operator] - def __imul__(self: NDFrameT, other) -> NDFrameT: + def __imul__(self, other) -> Self: # error: Unsupported left operand type for * ("Type[NDFrame]") return self._inplace_method(other, type(self).__mul__) # type: ignore[operator] - def __itruediv__(self: NDFrameT, other) -> NDFrameT: + def __itruediv__(self, other) -> Self: # error: Unsupported left operand type for / ("Type[NDFrame]") return self._inplace_method( other, type(self).__truediv__ # type: ignore[operator] ) - def __ifloordiv__(self: NDFrameT, other) -> NDFrameT: + def __ifloordiv__(self, other) -> Self: # error: Unsupported left operand type for // ("Type[NDFrame]") return self._inplace_method( other, type(self).__floordiv__ # type: ignore[operator] ) - def __imod__(self: NDFrameT, other) -> NDFrameT: + def __imod__(self, other) -> Self: # error: Unsupported left operand type for % ("Type[NDFrame]") return self._inplace_method(other, type(self).__mod__) # type: ignore[operator] - def __ipow__(self: NDFrameT, other) -> NDFrameT: + def __ipow__(self, other) -> Self: # error: Unsupported left operand type for ** ("Type[NDFrame]") return self._inplace_method(other, type(self).__pow__) # type: ignore[operator] - def __iand__(self: NDFrameT, other) -> NDFrameT: + def __iand__(self, other) -> Self: # error: Unsupported left operand type for & ("Type[NDFrame]") return self._inplace_method(other, type(self).__and__) # type: ignore[operator] - def __ior__(self: NDFrameT, other) -> NDFrameT: + def __ior__(self, other) -> Self: # error: Unsupported left operand type for | ("Type[NDFrame]") return self._inplace_method(other, type(self).__or__) # type: ignore[operator] - def __ixor__(self: NDFrameT, other) -> NDFrameT: + def __ixor__(self, other) -> Self: # error: Unsupported left operand type for ^ ("Type[NDFrame]") return self._inplace_method(other, type(self).__xor__) # type: ignore[operator] diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 2f658006cf93f..177af1a95fcb0 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -14,7 +14,6 @@ Literal, NoReturn, Sequence, - TypeVar, cast, final, overload, @@ -55,6 +54,7 @@ IndexLabel, JoinHow, Level, + Self, Shape, npt, ) @@ -293,9 +293,6 @@ def _new_Index(cls, d): return cls.__new__(cls, **d) -_IndexT = TypeVar("_IndexT", bound="Index") - - class Index(IndexOpsMixin, PandasObject): """ Immutable sequence used for indexing and alignment. @@ -361,7 +358,7 @@ class Index(IndexOpsMixin, PandasObject): # given the dtypes of the passed arguments @final - def _left_indexer_unique(self: _IndexT, other: _IndexT) -> npt.NDArray[np.intp]: + def _left_indexer_unique(self, other: Self) -> npt.NDArray[np.intp]: # Caller is responsible for ensuring other.dtype == self.dtype sv = self._get_join_target() ov = other._get_join_target() @@ -373,7 +370,7 @@ def _left_indexer_unique(self: _IndexT, other: _IndexT) -> npt.NDArray[np.intp]: @final def _left_indexer( - self: _IndexT, other: _IndexT + self, other: Self ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]: # Caller is responsible for ensuring other.dtype == self.dtype sv = self._get_join_target() @@ -387,7 +384,7 @@ def _left_indexer( @final def _inner_indexer( - self: _IndexT, other: _IndexT + self, other: Self ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]: # Caller is responsible for ensuring other.dtype == self.dtype sv = self._get_join_target() @@ -401,7 +398,7 @@ def _inner_indexer( @final def _outer_indexer( - self: _IndexT, other: _IndexT + self, other: Self ) -> tuple[ArrayLike, npt.NDArray[np.intp], npt.NDArray[np.intp]]: # Caller is responsible for ensuring other.dtype == self.dtype sv = self._get_join_target() @@ -632,9 +629,7 @@ def _dtype_to_subclass(cls, dtype: DtypeObj): # See each method's docstring. @classmethod - def _simple_new( - cls: type[_IndexT], values: ArrayLike, name: Hashable = None - ) -> _IndexT: + def _simple_new(cls, values: ArrayLike, name: Hashable = None) -> Self: """ We require that we have a dtype compat for the values. If we are passed a non-dtype compat, then coerce using the constructor. @@ -670,7 +665,7 @@ def _with_infer(cls, *args, **kwargs): return result @cache_readonly - def _constructor(self: _IndexT) -> type[_IndexT]: + def _constructor(self) -> type[Self]: return type(self) @final @@ -729,7 +724,7 @@ def _format_duplicate_message(self) -> DataFrame: # -------------------------------------------------------------------- # Index Internals Methods - def _shallow_copy(self: _IndexT, values, name: Hashable = no_default) -> _IndexT: + def _shallow_copy(self, values, name: Hashable = no_default) -> Self: """ Create a new Index with the same class as the caller, don't copy the data, use the same object attributes with passed in attributes taking @@ -746,7 +741,7 @@ def _shallow_copy(self: _IndexT, values, name: Hashable = no_default) -> _IndexT return self._simple_new(values, name=name) - def _view(self: _IndexT) -> _IndexT: + def _view(self) -> Self: """ fastpath to make a shallow copy, i.e. new object with same data. """ @@ -756,7 +751,7 @@ def _view(self: _IndexT) -> _IndexT: return result @final - def _rename(self: _IndexT, name: Hashable) -> _IndexT: + def _rename(self, name: Hashable) -> Self: """ fastpath for rename if new name is already validated. """ @@ -1154,10 +1149,10 @@ def repeat(self, repeats, axis=None): # Copying Methods def copy( - self: _IndexT, + self, name: Hashable | None = None, deep: bool = False, - ) -> _IndexT: + ) -> Self: """ Make a copy of this object. @@ -1189,11 +1184,11 @@ def copy( return new_index @final - def __copy__(self: _IndexT, **kwargs) -> _IndexT: + def __copy__(self, **kwargs) -> Self: return self.copy(**kwargs) @final - def __deepcopy__(self: _IndexT, memo=None) -> _IndexT: + def __deepcopy__(self, memo=None) -> Self: """ Parameters ---------- @@ -1414,7 +1409,7 @@ def _summary(self, name=None) -> str_t: # -------------------------------------------------------------------- # Conversion Methods - def to_flat_index(self: _IndexT) -> _IndexT: + def to_flat_index(self) -> Self: """ Identity method. @@ -1679,9 +1674,7 @@ def _set_names(self, values, *, level=None) -> None: names = property(fset=_set_names, fget=_get_names) @overload - def set_names( - self: _IndexT, names, *, level=..., inplace: Literal[False] = ... - ) -> _IndexT: + def set_names(self, names, *, level=..., inplace: Literal[False] = ...) -> Self: ... @overload @@ -1689,14 +1682,10 @@ def set_names(self, names, *, level=..., inplace: Literal[True]) -> None: ... @overload - def set_names( - self: _IndexT, names, *, level=..., inplace: bool = ... - ) -> _IndexT | None: + def set_names(self, names, *, level=..., inplace: bool = ...) -> Self | None: ... - def set_names( - self: _IndexT, names, *, level=None, inplace: bool = False - ) -> _IndexT | None: + def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None: """ Set Index or MultiIndex name. @@ -1862,7 +1851,7 @@ def nlevels(self) -> int: """ return 1 - def _sort_levels_monotonic(self: _IndexT) -> _IndexT: + def _sort_levels_monotonic(self) -> Self: """ Compat with MultiIndex. """ @@ -2849,7 +2838,7 @@ def fillna(self, value=None, downcast=None): ) return self._view() - def dropna(self: _IndexT, how: AnyAll = "any") -> _IndexT: + def dropna(self, how: AnyAll = "any") -> Self: """ Return Index without NA/NaN values. @@ -2874,7 +2863,7 @@ def dropna(self: _IndexT, how: AnyAll = "any") -> _IndexT: # -------------------------------------------------------------------- # Uniqueness Methods - def unique(self: _IndexT, level: Hashable | None = None) -> _IndexT: + def unique(self, level: Hashable | None = None) -> Self: """ Return unique values in the index. @@ -2904,7 +2893,7 @@ def unique(self: _IndexT, level: Hashable | None = None) -> _IndexT: result = super().unique() return self._shallow_copy(result) - def drop_duplicates(self: _IndexT, *, keep: DropKeep = "first") -> _IndexT: + def drop_duplicates(self, *, keep: DropKeep = "first") -> Self: """ Return Index with duplicate values removed. @@ -4854,18 +4843,18 @@ def _join_monotonic( return join_index, lidx, ridx def _wrap_joined_index( - self: _IndexT, + self, joined: ArrayLike, - other: _IndexT, + other: Self, lidx: npt.NDArray[np.intp], ridx: npt.NDArray[np.intp], - ) -> _IndexT: + ) -> Self: assert other.dtype == self.dtype if isinstance(self, ABCMultiIndex): name = self.names if self.names == other.names else None # error: Incompatible return value type (got "MultiIndex", - # expected "_IndexT") + # expected "Self") mask = lidx == -1 join_idx = self.take(lidx) right = other.take(ridx) @@ -5189,7 +5178,7 @@ def __getitem__(self, key): # didn't override __getitem__ return self._constructor._simple_new(result, name=self._name) - def _getitem_slice(self: _IndexT, slobj: slice) -> _IndexT: + def _getitem_slice(self, slobj: slice) -> Self: """ Fastpath for __getitem__ when we know we have a slice. """ @@ -6561,7 +6550,7 @@ def slice_locs(self, start=None, end=None, step=None) -> tuple[int, int]: return start_slice, end_slice - def delete(self: _IndexT, loc) -> _IndexT: + def delete(self, loc) -> Self: """ Make new Index with passed location(-s) deleted. diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 17fac22f578db..1133ea6be26ac 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -12,7 +12,6 @@ Any, Callable, Sequence, - TypeVar, cast, final, ) @@ -69,6 +68,7 @@ from pandas._typing import ( Axis, + Self, npt, ) @@ -76,9 +76,6 @@ _index_doc_kwargs = dict(ibase._index_doc_kwargs) -_T = TypeVar("_T", bound="DatetimeIndexOpsMixin") -_TDT = TypeVar("_TDT", bound="DatetimeTimedeltaMixin") - class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC): """ @@ -358,7 +355,7 @@ def _maybe_cast_slice_bound(self, label, side: str): # -------------------------------------------------------------------- # Arithmetic Methods - def shift(self: _T, periods: int = 1, freq=None) -> _T: + def shift(self, periods: int = 1, freq=None) -> Self: """ Shift index by desired number of time frequency increments. @@ -424,7 +421,7 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC): def unit(self) -> str: return self._data.unit - def as_unit(self: _TDT, unit: str) -> _TDT: + def as_unit(self, unit: str) -> Self: """ Convert to a dtype with the given unit resolution. @@ -449,7 +446,7 @@ def values(self) -> np.ndarray: return self._data._ndarray @doc(DatetimeIndexOpsMixin.shift) - def shift(self: _TDT, periods: int = 1, freq=None) -> _TDT: + def shift(self, periods: int = 1, freq=None) -> Self: if freq is not None and freq != self.freq: if isinstance(freq, str): freq = to_offset(freq) @@ -569,7 +566,7 @@ def _fast_intersect(self, other, sort): return result - def _can_fast_intersect(self: _T, other: _T) -> bool: + def _can_fast_intersect(self, other: Self) -> bool: # Note: we only get here with len(self) > 0 and len(other) > 0 if self.freq is None: return False @@ -587,7 +584,7 @@ def _can_fast_intersect(self: _T, other: _T) -> bool: # GH#42104 return self.freq.n == 1 - def _can_fast_union(self: _T, other: _T) -> bool: + def _can_fast_union(self, other: Self) -> bool: # Assumes that type(self) == type(other), as per the annotation # The ability to fast_union also implies that `freq` should be # retained on union. @@ -617,7 +614,7 @@ def _can_fast_union(self: _T, other: _T) -> bool: # Only need to "adjoin", not overlap return (right_start == left_end + freq) or right_start in left - def _fast_union(self: _TDT, other: _TDT, sort=None) -> _TDT: + def _fast_union(self, other: Self, sort=None) -> Self: # Caller is responsible for ensuring self and other are non-empty # to make our life easier, "sort" the two ranges diff --git a/pandas/core/indexes/extension.py b/pandas/core/indexes/extension.py index 7d4dcf54a025f..61949531f37df 100644 --- a/pandas/core/indexes/extension.py +++ b/pandas/core/indexes/extension.py @@ -26,7 +26,6 @@ from pandas.core.arrays import IntervalArray from pandas.core.arrays._mixins import NDArrayBackedExtensionArray -_T = TypeVar("_T", bound="NDArrayBackedExtensionIndex") _ExtensionIndexT = TypeVar("_ExtensionIndexT", bound="ExtensionIndex") diff --git a/pandas/core/series.py b/pandas/core/series.py index 58cd42eaa7ca3..ac80a4a576bc7 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -173,6 +173,7 @@ QuantileInterpolation, Renamer, Scalar, + Self, SingleManager, SortKind, StorageOptions, @@ -4280,7 +4281,7 @@ def map( self, method="map" ) - def _gotitem(self, key, ndim, subset=None) -> Series: + def _gotitem(self, key, ndim, subset=None) -> Self: """ Sub-classes to define. Return a sliced object. @@ -4597,7 +4598,7 @@ def align( limit: int | None = None, fill_axis: Axis = 0, broadcast_axis: Axis | None = None, - ) -> tuple[Series, NDFrameT]: + ) -> tuple[Self, NDFrameT]: return super().align( other, join=join,