diff --git a/pandas/_libs/internals.pyx b/pandas/_libs/internals.pyx index 533727f8f2d42..88c95331cd393 100644 --- a/pandas/_libs/internals.pyx +++ b/pandas/_libs/internals.pyx @@ -831,7 +831,7 @@ cdef class BlockManager: # ------------------------------------------------------------------- # Indexing - cdef BlockManager _get_index_slice(self, slobj): + cdef BlockManager _get_index_slice(self, slice slobj): cdef: SharedBlock blk, nb BlockManager mgr diff --git a/pandas/_libs/lib.pyi b/pandas/_libs/lib.pyi index 31d4274bb5f8d..2e425f5797c62 100644 --- a/pandas/_libs/lib.pyi +++ b/pandas/_libs/lib.pyi @@ -47,6 +47,7 @@ def is_decimal(val: object) -> TypeGuard[Decimal]: ... def is_complex(val: object) -> TypeGuard[complex]: ... def is_bool(val: object) -> TypeGuard[bool | np.bool_]: ... def is_integer(val: object) -> TypeGuard[int | np.integer]: ... +def is_int_or_none(obj) -> bool: ... def is_float(val: object) -> TypeGuard[float]: ... def is_interval_array(values: np.ndarray) -> bool: ... def is_datetime64_array(values: np.ndarray) -> bool: ... diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index c6aded1b25281..573f5aca6aff6 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -1057,6 +1057,17 @@ def is_integer(obj: object) -> bool: return util.is_integer_object(obj) +def is_int_or_none(obj) -> bool: + """ + Return True if given object is integer or None. + + Returns + ------- + bool + """ + return obj is None or util.is_integer_object(obj) + + def is_bool(obj: object) -> bool: """ Return True if given object is boolean. diff --git a/pandas/core/indexers/utils.py b/pandas/core/indexers/utils.py index ffd33a39b8d2b..55bb58f3108c3 100644 --- a/pandas/core/indexers/utils.py +++ b/pandas/core/indexers/utils.py @@ -10,6 +10,8 @@ import numpy as np +from pandas._libs import lib + from pandas.core.dtypes.common import ( is_array_like, is_bool_dtype, @@ -50,14 +52,10 @@ def is_valid_positional_slice(slc: slice) -> bool: A valid positional slice may also be interpreted as a label-based slice depending on the index being sliced. """ - - def is_int_or_none(val): - return val is None or is_integer(val) - return ( - is_int_or_none(slc.start) - and is_int_or_none(slc.stop) - and is_int_or_none(slc.step) + lib.is_int_or_none(slc.start) + and lib.is_int_or_none(slc.stop) + and lib.is_int_or_none(slc.step) ) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index eb79278eb35d9..e615d9055efc4 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -99,7 +99,6 @@ is_float_dtype, is_hashable, is_integer, - is_integer_dtype, is_iterator, is_list_like, is_numeric_dtype, @@ -161,7 +160,10 @@ extract_array, sanitize_array, ) -from pandas.core.indexers import disallow_ndim_indexing +from pandas.core.indexers import ( + disallow_ndim_indexing, + is_valid_positional_slice, +) from pandas.core.indexes.frozen import FrozenList from pandas.core.missing import clean_reindex_fill_method from pandas.core.ops import get_op_result_name @@ -4071,7 +4073,7 @@ def _validate_positional_slice(self, key: slice) -> None: self._validate_indexer("positional", key.stop, "iloc") self._validate_indexer("positional", key.step, "iloc") - def _convert_slice_indexer(self, key: slice, kind: str_t): + def _convert_slice_indexer(self, key: slice, kind: Literal["loc", "getitem"]): """ Convert a slice indexer. @@ -4083,7 +4085,6 @@ def _convert_slice_indexer(self, key: slice, kind: str_t): key : label of the slice bound kind : {'loc', 'getitem'} """ - assert kind in ["loc", "getitem"], kind # potentially cast the bounds to integers start, stop, step = key.start, key.stop, key.step @@ -4096,22 +4097,14 @@ def _convert_slice_indexer(self, key: slice, kind: str_t): return self.slice_indexer(start, stop, step) # figure out if this is a positional indexer - def is_int(v): - return v is None or is_integer(v) - - is_index_slice = is_int(start) and is_int(stop) and is_int(step) - - # special case for interval_dtype bc we do not do partial-indexing - # on integer Intervals when slicing - # TODO: write this in terms of e.g. should_partial_index? - ints_are_positional = self._should_fallback_to_positional or isinstance( - self.dtype, IntervalDtype - ) - is_positional = is_index_slice and ints_are_positional + is_index_slice = is_valid_positional_slice(key) if kind == "getitem": # called from the getitem slicers, validate that we are in fact integers - if is_index_slice or is_integer_dtype(self.dtype): + if is_index_slice: + # In this case the _validate_indexer checks below are redundant + return key + elif self.dtype.kind in "iu": # Note: these checks are redundant if we know is_index_slice self._validate_indexer("slice", key.start, "getitem") self._validate_indexer("slice", key.stop, "getitem") @@ -4120,6 +4113,14 @@ def is_int(v): # convert the slice to an indexer here + # special case for interval_dtype bc we do not do partial-indexing + # on integer Intervals when slicing + # TODO: write this in terms of e.g. should_partial_index? + ints_are_positional = self._should_fallback_to_positional or isinstance( + self.dtype, IntervalDtype + ) + is_positional = is_index_slice and ints_are_positional + # if we are mixed and have integers if is_positional: try: @@ -4151,7 +4152,7 @@ def is_int(v): @final def _raise_invalid_indexer( self, - form: str_t, + form: Literal["slice", "positional"], key, reraise: lib.NoDefault | None | Exception = lib.no_default, ) -> None: @@ -6384,14 +6385,17 @@ def _maybe_cast_listlike_indexer(self, target) -> Index: return ensure_index(target) @final - def _validate_indexer(self, form: str_t, key, kind: str_t) -> None: + def _validate_indexer( + self, + form: Literal["positional", "slice"], + key, + kind: Literal["getitem", "iloc"], + ) -> None: """ If we are positional indexer, validate that we have appropriate typed bounds must be an integer. """ - assert kind in ["getitem", "iloc"] - - if key is not None and not is_integer(key): + if not lib.is_int_or_none(key): self._raise_invalid_indexer(form, key) def _maybe_cast_slice_bound(self, label, side: str_t): diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 1740c5c368a94..ede3b8f0c0e95 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -784,7 +784,7 @@ def _index_as_unique(self) -> bool: "cannot handle overlapping indices; use IntervalIndex.get_indexer_non_unique" ) - def _convert_slice_indexer(self, key: slice, kind: str): + def _convert_slice_indexer(self, key: slice, kind: Literal["loc", "getitem"]): if not (key.step is None or key.step == 1): # GH#31658 if label-based, we require step == 1, # if positional, we disallow float start/stop diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index 66c5a12549f23..8ed9543cc00dd 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -54,6 +54,7 @@ npt, ) _empty_range = range(0) +_dtype_int64 = np.dtype(np.int64) class RangeIndex(Index): @@ -309,7 +310,7 @@ def memory_usage(self, deep: bool = False) -> int: @property def dtype(self) -> np.dtype: - return np.dtype(np.int64) + return _dtype_int64 @property def is_unique(self) -> bool: