diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index cb68bd0e762c4..30a828064f812 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -343,6 +343,7 @@ Numeric - Bug in :class:`DataFrame` logical operations (`&`, `|`, `^`) not matching :class:`Series` behavior by filling NA values (:issue:`28741`) - Bug in :meth:`DataFrame.interpolate` where specifying axis by name references variable before it is assigned (:issue:`29142`) - Improved error message when using `frac` > 1 and `replace` = False (:issue:`27451`) +- Bug in numeric indexes resulted in it being possible to instantiate an :class:`Int64Index`, :class:`UInt64Index`, or :class:`Float64Index` with an invalid dtype (e.g. datetime-like) (:issue:`29539`) - Bug in :class:`UInt64Index` precision loss while constructing from a list with values in the ``np.uint64`` range (:issue:`29526`) - diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 86664a14e91dd..8978a09825ee9 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -381,7 +381,7 @@ def __new__( pass # Return an actual float index. - return Float64Index(data, copy=copy, dtype=dtype, name=name) + return Float64Index(data, copy=copy, name=name) elif inferred == "string": pass diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index 074cce085fb3c..29f56259dac79 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -15,6 +15,8 @@ is_float_dtype, is_integer_dtype, is_scalar, + is_signed_integer_dtype, + is_unsigned_integer_dtype, needs_i8_conversion, pandas_dtype, ) @@ -27,6 +29,7 @@ ) from pandas.core.dtypes.missing import isna +from pandas._typing import Dtype from pandas.core import algorithms import pandas.core.common as com from pandas.core.indexes.base import Index, InvalidIndexError, _index_shared_docs @@ -45,7 +48,7 @@ class NumericIndex(Index): _is_numeric_dtype = True def __new__(cls, data=None, dtype=None, copy=False, name=None, fastpath=None): - + cls._validate_dtype(dtype) if fastpath is not None: warnings.warn( "The 'fastpath' keyword is deprecated, and will be " @@ -80,6 +83,22 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None, fastpath=None): name = data.name return cls._simple_new(subarr, name=name) + @classmethod + def _validate_dtype(cls, dtype: Dtype) -> None: + if dtype is None: + return + validation_metadata = { + "int64index": (is_signed_integer_dtype, "signed integer"), + "uint64index": (is_unsigned_integer_dtype, "unsigned integer"), + "float64index": (is_float_dtype, "float"), + "rangeindex": (is_signed_integer_dtype, "signed integer"), + } + + validation_func, expected = validation_metadata[cls._typ] + if not validation_func(dtype): + msg = f"Incorrect `dtype` passed: expected {expected}, received {dtype}" + raise ValueError(msg) + @Appender(_index_shared_docs["_maybe_cast_slice_bound"]) def _maybe_cast_slice_bound(self, label, side, kind): assert kind in ["ix", "loc", "getitem", None] diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index d200ff6a71264..6f677848b1c79 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -14,7 +14,6 @@ from pandas.core.dtypes.common import ( ensure_platform_int, ensure_python_int, - is_int64_dtype, is_integer, is_integer_dtype, is_list_like, @@ -165,12 +164,6 @@ def _simple_new(cls, values, name=None, dtype=None): # -------------------------------------------------------------------- - @staticmethod - def _validate_dtype(dtype): - """ require dtype to be None or int64 """ - if not (dtype is None or is_int64_dtype(dtype)): - raise TypeError("Invalid to pass a non-int64 dtype to RangeIndex") - @cache_readonly def _constructor(self): """ return the class to use for construction """ diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py index e0f7b1d1ade3d..6ee1ce5c4f2ad 100644 --- a/pandas/tests/indexes/test_numeric.py +++ b/pandas/tests/indexes/test_numeric.py @@ -167,6 +167,23 @@ def test_constructor(self): result = Index(np.array([np.nan])) assert pd.isna(result.values).all() + @pytest.mark.parametrize( + "index, dtype", + [ + (pd.Int64Index, "float64"), + (pd.UInt64Index, "categorical"), + (pd.Float64Index, "datetime64"), + (pd.RangeIndex, "float64"), + ], + ) + def test_invalid_dtype(self, index, dtype): + # GH 29539 + with pytest.raises( + ValueError, + match=rf"Incorrect `dtype` passed: expected \w+(?: \w+)?, received {dtype}", + ): + index([1, 2, 3], dtype=dtype) + def test_constructor_invalid(self): # invalid diff --git a/pandas/tests/indexes/test_range.py b/pandas/tests/indexes/test_range.py index fa64e1bacb2e5..b60d3126da1d5 100644 --- a/pandas/tests/indexes/test_range.py +++ b/pandas/tests/indexes/test_range.py @@ -110,7 +110,10 @@ def test_constructor_same(self): result = RangeIndex(index) tm.assert_index_equal(result, index, exact=True) - with pytest.raises(TypeError): + with pytest.raises( + ValueError, + match="Incorrect `dtype` passed: expected signed integer, received float64", + ): RangeIndex(index, dtype="float64") def test_constructor_range(self): @@ -140,7 +143,10 @@ def test_constructor_range(self): expected = RangeIndex(1, 5, 2) tm.assert_index_equal(result, expected, exact=True) - with pytest.raises(TypeError): + with pytest.raises( + ValueError, + match="Incorrect `dtype` passed: expected signed integer, received float64", + ): Index(range(1, 5, 2), dtype="float64") msg = r"^from_range\(\) got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): @@ -178,7 +184,10 @@ def test_constructor_corner(self): RangeIndex(1.1, 10.2, 1.3) # invalid passed type - with pytest.raises(TypeError): + with pytest.raises( + ValueError, + match="Incorrect `dtype` passed: expected signed integer, received float64", + ): RangeIndex(1, 5, dtype="float64") @pytest.mark.parametrize( diff --git a/pandas/tests/series/indexing/test_numeric.py b/pandas/tests/series/indexing/test_numeric.py index bcddcf843df06..60b89c01cc22d 100644 --- a/pandas/tests/series/indexing/test_numeric.py +++ b/pandas/tests/series/indexing/test_numeric.py @@ -86,8 +86,7 @@ def test_get(): 1764.0, 1849.0, 1936.0, - ], - dtype="object", + ] ), )