diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 4c85b3d5dc745..be31a78776970 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -191,6 +191,7 @@ Removal of prior version deprecations/changes - Removed argument ``tz`` from :meth:`Period.to_timestamp`, use ``obj.to_timestamp(...).tz_localize(tz)`` instead (:issue:`34522`) - Removed argument ``is_copy`` from :meth:`DataFrame.take` and :meth:`Series.take` (:issue:`30615`) - Removed argument ``kind`` from :meth:`Index.get_slice_bound`, :meth:`Index.slice_indexer` and :meth:`Index.slice_locs` (:issue:`41378`) +- Disallow subclass-specific keywords (e.g. "freq", "tz", "names", "closed") in the :class:`Index` constructor (:issue:`38597`) - Removed argument ``inplace`` from :meth:`Categorical.remove_unused_categories` (:issue:`37918`) - Disallow passing non-round floats to :class:`Timestamp` with ``unit="M"`` or ``unit="Y"`` (:issue:`47266`) - Remove keywords ``convert_float`` and ``mangle_dupe_cols`` from :func:`read_excel` (:issue:`41176`) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 2fe61623abfd5..36a66533018bd 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -152,10 +152,6 @@ Categorical, ExtensionArray, ) -from pandas.core.arrays.datetimes import ( - tz_to_dtype, - validate_tz_from_dtype, -) from pandas.core.arrays.string_ import StringArray from pandas.core.base import ( IndexOpsMixin, @@ -241,11 +237,6 @@ def join( return cast(F, join) -def disallow_kwargs(kwargs: dict[str, Any]) -> None: - if kwargs: - raise TypeError(f"Unexpected keyword arguments {repr(set(kwargs))}") - - def _new_Index(cls, d): """ This is called upon unpickling, rather than the default which doesn't @@ -437,18 +428,8 @@ def __new__( copy: bool = False, name=None, tupleize_cols: bool = True, - **kwargs, ) -> Index: - if kwargs: - warnings.warn( - "Passing keywords other than 'data', 'dtype', 'copy', 'name', " - "'tupleize_cols' is deprecated and will raise TypeError in a " - "future version. Use the specific Index subclass directly instead.", - FutureWarning, - stacklevel=find_stack_level(), - ) - from pandas.core.arrays import PandasArray from pandas.core.indexes.range import RangeIndex @@ -456,10 +437,6 @@ def __new__( if dtype is not None: dtype = pandas_dtype(dtype) - if "tz" in kwargs: - tz = kwargs.pop("tz") - validate_tz_from_dtype(dtype, tz) - dtype = tz_to_dtype(tz) if type(data) is PandasArray: # ensure users don't accidentally put a PandasArray in an index, @@ -481,18 +458,17 @@ def __new__( # non-EA dtype indexes have special casting logic, so we punt here klass = cls._dtype_to_subclass(dtype) if klass is not Index: - return klass(data, dtype=dtype, copy=copy, name=name, **kwargs) + return klass(data, dtype=dtype, copy=copy, name=name) ea_cls = dtype.construct_array_type() data = ea_cls._from_sequence(data, dtype=dtype, copy=copy) - disallow_kwargs(kwargs) return Index._simple_new(data, name=name) elif is_ea_or_datetimelike_dtype(data_dtype): data_dtype = cast(DtypeObj, data_dtype) klass = cls._dtype_to_subclass(data_dtype) if klass is not Index: - result = klass(data, copy=copy, name=name, **kwargs) + result = klass(data, copy=copy, name=name) if dtype is not None: return result.astype(dtype, copy=False) return result @@ -500,7 +476,6 @@ def __new__( # GH#45206 data = data.astype(dtype, copy=False) - disallow_kwargs(kwargs) data = extract_array(data, extract_numpy=True) return Index._simple_new(data, name=name) @@ -542,18 +517,14 @@ def __new__( ) dtype = arr.dtype - if kwargs: - return cls(arr, dtype, copy=copy, name=name, **kwargs) - klass = cls._dtype_to_subclass(arr.dtype) arr = klass._ensure_array(arr, dtype, copy) - disallow_kwargs(kwargs) return klass._simple_new(arr, name) elif is_scalar(data): raise cls._raise_scalar_data_error(data) elif hasattr(data, "__array__"): - return Index(np.asarray(data), dtype=dtype, copy=copy, name=name, **kwargs) + return Index(np.asarray(data), dtype=dtype, copy=copy, name=name) else: if tupleize_cols and is_list_like(data): @@ -566,9 +537,7 @@ def __new__( # 10697 from pandas.core.indexes.multi import MultiIndex - return MultiIndex.from_tuples( - data, names=name or kwargs.get("names") - ) + return MultiIndex.from_tuples(data, names=name) # other iterable of some kind subarr = com.asarray_tuplesafe(data, dtype=_dtype_obj) @@ -578,7 +547,7 @@ def __new__( subarr, cast_numeric_deprecated=False ) dtype = subarr.dtype - return Index(subarr, dtype=dtype, copy=copy, name=name, **kwargs) + return Index(subarr, dtype=dtype, copy=copy, name=name) @classmethod def _ensure_array(cls, data, dtype, copy: bool): @@ -7081,19 +7050,6 @@ def shape(self) -> Shape: # See GH#27775, GH#27384 for history/reasoning in how this is defined. return (len(self),) - @final - def _deprecated_arg(self, value, name: str_t, methodname: str_t) -> None: - """ - Issue a FutureWarning if the arg/kwarg is not no_default. - """ - if value is not no_default: - warnings.warn( - f"'{name}' argument in {methodname} is deprecated " - "and will be removed in a future version. Do not pass it.", - FutureWarning, - stacklevel=find_stack_level(), - ) - def ensure_index_from_sequences(sequences, names=None) -> Index: """ @@ -7246,14 +7202,6 @@ def maybe_extract_name(name, obj, cls) -> Hashable: return name -_cast_depr_msg = ( - "In a future version, passing an object-dtype arraylike to pd.Index will " - "not infer numeric values to numeric dtype (matching the Series behavior). " - "To retain the old behavior, explicitly pass the desired dtype or use the " - "desired Index subclass" -) - - def _maybe_cast_data_without_dtype( subarr: np.ndarray, cast_numeric_deprecated: bool = True ) -> ArrayLike: diff --git a/pandas/tests/indexes/base_class/test_constructors.py b/pandas/tests/indexes/base_class/test_constructors.py index df04502a01f99..cf8b7214f3b91 100644 --- a/pandas/tests/indexes/base_class/test_constructors.py +++ b/pandas/tests/indexes/base_class/test_constructors.py @@ -30,12 +30,6 @@ def test_construction_list_mixed_tuples(self, index_vals): assert isinstance(index, Index) assert not isinstance(index, MultiIndex) - def test_constructor_wrong_kwargs(self): - # GH #19348 - with pytest.raises(TypeError, match="Unexpected keyword arguments {'foo'}"): - with tm.assert_produces_warning(FutureWarning): - Index([], foo="bar") - def test_constructor_cast(self): msg = "could not convert string to float" with pytest.raises(ValueError, match=msg): diff --git a/pandas/tests/indexes/categorical/test_constructors.py b/pandas/tests/indexes/categorical/test_constructors.py index 98da8038401e7..def865e03ed7c 100644 --- a/pandas/tests/indexes/categorical/test_constructors.py +++ b/pandas/tests/indexes/categorical/test_constructors.py @@ -145,15 +145,5 @@ def test_construction_with_categorical_dtype(self): with pytest.raises(ValueError, match=msg): CategoricalIndex(data, categories=cats, dtype=dtype) - with pytest.raises(ValueError, match=msg): - with tm.assert_produces_warning(FutureWarning): - # passing subclass-specific kwargs to pd.Index - Index(data, categories=cats, dtype=dtype) - with pytest.raises(ValueError, match=msg): CategoricalIndex(data, ordered=ordered, dtype=dtype) - - with pytest.raises(ValueError, match=msg): - with tm.assert_produces_warning(FutureWarning): - # passing subclass-specific kwargs to pd.Index - Index(data, ordered=ordered, dtype=dtype) diff --git a/pandas/tests/indexes/datetimes/test_constructors.py b/pandas/tests/indexes/datetimes/test_constructors.py index 67f323c5afd81..0061cfd2b903f 100644 --- a/pandas/tests/indexes/datetimes/test_constructors.py +++ b/pandas/tests/indexes/datetimes/test_constructors.py @@ -409,17 +409,6 @@ def test_construction_index_with_mixed_timezones_with_NaT(self): assert isinstance(result, DatetimeIndex) assert result.tz is None - # all NaT with tz - with tm.assert_produces_warning(FutureWarning): - # subclass-specific kwargs to pd.Index - result = Index([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx") - exp = DatetimeIndex([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx") - - tm.assert_index_equal(result, exp, exact=True) - assert isinstance(result, DatetimeIndex) - assert result.tz is not None - assert result.tz == exp.tz - def test_construction_dti_with_mixed_timezones(self): # GH 11488 (not changed, added explicit tests) @@ -497,22 +486,6 @@ def test_construction_dti_with_mixed_timezones(self): name="idx", ) - with pytest.raises(ValueError, match=msg): - # passing tz should results in DatetimeIndex, then mismatch raises - # TypeError - with tm.assert_produces_warning(FutureWarning): - # subclass-specific kwargs to pd.Index - Index( - [ - pd.NaT, - Timestamp("2011-01-01 10:00"), - pd.NaT, - Timestamp("2011-01-02 10:00", tz="US/Eastern"), - ], - tz="Asia/Tokyo", - name="idx", - ) - def test_construction_base_constructor(self): arr = [Timestamp("2011-01-01"), pd.NaT, Timestamp("2011-01-03")] tm.assert_index_equal(Index(arr), DatetimeIndex(arr)) diff --git a/pandas/tests/indexes/interval/test_constructors.py b/pandas/tests/indexes/interval/test_constructors.py index a71a8f9e34ea9..315578a5dbf50 100644 --- a/pandas/tests/indexes/interval/test_constructors.py +++ b/pandas/tests/indexes/interval/test_constructors.py @@ -38,7 +38,6 @@ class ConstructorTests: get_kwargs_from_breaks to the expected format. """ - @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize( "breaks", [ @@ -96,22 +95,16 @@ def test_constructor_dtype(self, constructor, breaks, subtype): ) def test_constructor_pass_closed(self, constructor, breaks): # not passing closed to IntervalDtype, but to IntervalArray constructor - warn = None - if isinstance(constructor, partial) and constructor.func is Index: - # passing kwargs to Index is deprecated - warn = FutureWarning - iv_dtype = IntervalDtype(breaks.dtype) result_kwargs = self.get_kwargs_from_breaks(breaks) for dtype in (iv_dtype, str(iv_dtype)): - with tm.assert_produces_warning(warn): + with tm.assert_produces_warning(None): result = constructor(dtype=dtype, closed="left", **result_kwargs) assert result.dtype.closed == "left" - @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize("breaks", [[np.nan] * 2, [np.nan] * 4, [np.nan] * 50]) def test_constructor_nan(self, constructor, breaks, closed): # GH 18421 @@ -125,7 +118,6 @@ def test_constructor_nan(self, constructor, breaks, closed): assert result.dtype.subtype == expected_subtype tm.assert_numpy_array_equal(np.array(result), expected_values) - @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize( "breaks", [ @@ -353,9 +345,14 @@ class TestClassConstructors(ConstructorTests): params=[IntervalIndex, partial(Index, dtype="interval")], ids=["IntervalIndex", "Index"], ) - def constructor(self, request): + def klass(self, request): + # We use a separate fixture here to include Index.__new__ with dtype kwarg return request.param + @pytest.fixture + def constructor(self): + return IntervalIndex + def get_kwargs_from_breaks(self, breaks, closed="right"): """ converts intervals in breaks format to a dictionary of kwargs to @@ -388,12 +385,12 @@ def test_constructor_string(self): # the interval of strings is already forbidden. pass - def test_constructor_errors(self, constructor): + def test_constructor_errors(self, klass): # mismatched closed within intervals with no constructor override ivs = [Interval(0, 1, closed="right"), Interval(2, 3, closed="left")] msg = "intervals must all be closed on the same side" with pytest.raises(ValueError, match=msg): - constructor(ivs) + klass(ivs) # scalar msg = ( @@ -401,14 +398,13 @@ def test_constructor_errors(self, constructor): "some kind, 5 was passed" ) with pytest.raises(TypeError, match=msg): - constructor(5) + klass(5) # not an interval; dtype depends on 32bit/windows builds msg = "type with value 0 is not an interval" with pytest.raises(TypeError, match=msg): - constructor([0, 1]) + klass([0, 1]) - @pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning") @pytest.mark.parametrize( "data, closed", [ diff --git a/pandas/tests/indexes/multi/test_equivalence.py b/pandas/tests/indexes/multi/test_equivalence.py index 18ff73dcb2221..c51b9386d7ec6 100644 --- a/pandas/tests/indexes/multi/test_equivalence.py +++ b/pandas/tests/indexes/multi/test_equivalence.py @@ -191,18 +191,7 @@ def test_identical(idx): mi2 = mi2.set_names(["new1", "new2"]) assert mi.identical(mi2) - with tm.assert_produces_warning(FutureWarning): - # subclass-specific keywords to pd.Index - mi3 = Index(mi.tolist(), names=mi.names) - - msg = r"Unexpected keyword arguments {'names'}" - with pytest.raises(TypeError, match=msg): - with tm.assert_produces_warning(FutureWarning): - # subclass-specific keywords to pd.Index - Index(mi.tolist(), names=mi.names, tupleize_cols=False) - mi4 = Index(mi.tolist(), tupleize_cols=False) - assert mi.identical(mi3) assert not mi.identical(mi4) assert mi.equals(mi4) diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index cfbc90d1b36bb..5e3a427bc75ba 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -56,9 +56,7 @@ def test_take_preserve_name(idx): def test_copy_names(): # Check that adding a "names" parameter to the copy is honored # GH14302 - with tm.assert_produces_warning(FutureWarning): - # subclass-specific kwargs to pd.Index - multi_idx = pd.Index([(1, 2), (3, 4)], names=["MyName1", "MyName2"]) + multi_idx = MultiIndex.from_tuples([(1, 2), (3, 4)], names=["MyName1", "MyName2"]) multi_idx1 = multi_idx.copy() assert multi_idx.equals(multi_idx1) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index bfe462cdf6c15..e5cce91b09a2f 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -250,9 +250,13 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass): if attr == "asi8": result = DatetimeIndex(arg).tz_localize(tz_naive_fixture) + tm.assert_index_equal(result, index) + elif klass is Index: + with pytest.raises(TypeError, match="unexpected keyword"): + klass(arg, tz=tz_naive_fixture) else: result = klass(arg, tz=tz_naive_fixture) - tm.assert_index_equal(result, index) + tm.assert_index_equal(result, index) if attr == "asi8": if err: @@ -267,9 +271,13 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass): if attr == "asi8": result = DatetimeIndex(list(arg)).tz_localize(tz_naive_fixture) + tm.assert_index_equal(result, index) + elif klass is Index: + with pytest.raises(TypeError, match="unexpected keyword"): + klass(arg, tz=tz_naive_fixture) else: result = klass(list(arg), tz=tz_naive_fixture) - tm.assert_index_equal(result, index) + tm.assert_index_equal(result, index) if attr == "asi8": if err: diff --git a/pandas/tests/indexing/test_coercion.py b/pandas/tests/indexing/test_coercion.py index 2d54a9ba370ca..26424904482d1 100644 --- a/pandas/tests/indexing/test_coercion.py +++ b/pandas/tests/indexing/test_coercion.py @@ -368,12 +368,6 @@ def test_insert_index_period(self, insert, coerced_val, coerced_dtype): expected = obj.astype(object).insert(0, str(insert)) tm.assert_index_equal(result, expected) - msg = r"Unexpected keyword arguments {'freq'}" - with pytest.raises(TypeError, match=msg): - with tm.assert_produces_warning(FutureWarning): - # passing keywords to pd.Index - pd.Index(data, freq="M") - @pytest.mark.xfail(reason="Test not implemented") def test_insert_index_complex128(self): raise NotImplementedError