Skip to content

Commit 032316f

Browse files
authored
DEPR: disallow subclass-specific keywords in Index.__new__ (#49311)
1 parent 3288d8f commit 032316f

File tree

10 files changed

+28
-137
lines changed

10 files changed

+28
-137
lines changed

doc/source/whatsnew/v2.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ Removal of prior version deprecations/changes
192192
- Removed argument ``sort_columns`` in :meth:`DataFrame.plot` and :meth:`Series.plot` (:issue:`47563`)
193193
- Removed argument ``is_copy`` from :meth:`DataFrame.take` and :meth:`Series.take` (:issue:`30615`)
194194
- Removed argument ``kind`` from :meth:`Index.get_slice_bound`, :meth:`Index.slice_indexer` and :meth:`Index.slice_locs` (:issue:`41378`)
195+
- Disallow subclass-specific keywords (e.g. "freq", "tz", "names", "closed") in the :class:`Index` constructor (:issue:`38597`)
195196
- Removed argument ``inplace`` from :meth:`Categorical.remove_unused_categories` (:issue:`37918`)
196197
- Disallow passing non-round floats to :class:`Timestamp` with ``unit="M"`` or ``unit="Y"`` (:issue:`47266`)
197198
- Remove keywords ``convert_float`` and ``mangle_dupe_cols`` from :func:`read_excel` (:issue:`41176`)

pandas/core/indexes/base.py

+5-57
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,6 @@
152152
Categorical,
153153
ExtensionArray,
154154
)
155-
from pandas.core.arrays.datetimes import (
156-
tz_to_dtype,
157-
validate_tz_from_dtype,
158-
)
159155
from pandas.core.arrays.string_ import StringArray
160156
from pandas.core.base import (
161157
IndexOpsMixin,
@@ -241,11 +237,6 @@ def join(
241237
return cast(F, join)
242238

243239

244-
def disallow_kwargs(kwargs: dict[str, Any]) -> None:
245-
if kwargs:
246-
raise TypeError(f"Unexpected keyword arguments {repr(set(kwargs))}")
247-
248-
249240
def _new_Index(cls, d):
250241
"""
251242
This is called upon unpickling, rather than the default which doesn't
@@ -437,29 +428,15 @@ def __new__(
437428
copy: bool = False,
438429
name=None,
439430
tupleize_cols: bool = True,
440-
**kwargs,
441431
) -> Index:
442432

443-
if kwargs:
444-
warnings.warn(
445-
"Passing keywords other than 'data', 'dtype', 'copy', 'name', "
446-
"'tupleize_cols' is deprecated and will raise TypeError in a "
447-
"future version. Use the specific Index subclass directly instead.",
448-
FutureWarning,
449-
stacklevel=find_stack_level(),
450-
)
451-
452433
from pandas.core.arrays import PandasArray
453434
from pandas.core.indexes.range import RangeIndex
454435

455436
name = maybe_extract_name(name, data, cls)
456437

457438
if dtype is not None:
458439
dtype = pandas_dtype(dtype)
459-
if "tz" in kwargs:
460-
tz = kwargs.pop("tz")
461-
validate_tz_from_dtype(dtype, tz)
462-
dtype = tz_to_dtype(tz)
463440

464441
if type(data) is PandasArray:
465442
# ensure users don't accidentally put a PandasArray in an index,
@@ -481,26 +458,24 @@ def __new__(
481458
# non-EA dtype indexes have special casting logic, so we punt here
482459
klass = cls._dtype_to_subclass(dtype)
483460
if klass is not Index:
484-
return klass(data, dtype=dtype, copy=copy, name=name, **kwargs)
461+
return klass(data, dtype=dtype, copy=copy, name=name)
485462

486463
ea_cls = dtype.construct_array_type()
487464
data = ea_cls._from_sequence(data, dtype=dtype, copy=copy)
488-
disallow_kwargs(kwargs)
489465
return Index._simple_new(data, name=name)
490466

491467
elif is_ea_or_datetimelike_dtype(data_dtype):
492468
data_dtype = cast(DtypeObj, data_dtype)
493469
klass = cls._dtype_to_subclass(data_dtype)
494470
if klass is not Index:
495-
result = klass(data, copy=copy, name=name, **kwargs)
471+
result = klass(data, copy=copy, name=name)
496472
if dtype is not None:
497473
return result.astype(dtype, copy=False)
498474
return result
499475
elif dtype is not None:
500476
# GH#45206
501477
data = data.astype(dtype, copy=False)
502478

503-
disallow_kwargs(kwargs)
504479
data = extract_array(data, extract_numpy=True)
505480
return Index._simple_new(data, name=name)
506481

@@ -542,18 +517,14 @@ def __new__(
542517
)
543518
dtype = arr.dtype
544519

545-
if kwargs:
546-
return cls(arr, dtype, copy=copy, name=name, **kwargs)
547-
548520
klass = cls._dtype_to_subclass(arr.dtype)
549521
arr = klass._ensure_array(arr, dtype, copy)
550-
disallow_kwargs(kwargs)
551522
return klass._simple_new(arr, name)
552523

553524
elif is_scalar(data):
554525
raise cls._raise_scalar_data_error(data)
555526
elif hasattr(data, "__array__"):
556-
return Index(np.asarray(data), dtype=dtype, copy=copy, name=name, **kwargs)
527+
return Index(np.asarray(data), dtype=dtype, copy=copy, name=name)
557528
else:
558529

559530
if tupleize_cols and is_list_like(data):
@@ -566,9 +537,7 @@ def __new__(
566537
# 10697
567538
from pandas.core.indexes.multi import MultiIndex
568539

569-
return MultiIndex.from_tuples(
570-
data, names=name or kwargs.get("names")
571-
)
540+
return MultiIndex.from_tuples(data, names=name)
572541
# other iterable of some kind
573542

574543
subarr = com.asarray_tuplesafe(data, dtype=_dtype_obj)
@@ -578,7 +547,7 @@ def __new__(
578547
subarr, cast_numeric_deprecated=False
579548
)
580549
dtype = subarr.dtype
581-
return Index(subarr, dtype=dtype, copy=copy, name=name, **kwargs)
550+
return Index(subarr, dtype=dtype, copy=copy, name=name)
582551

583552
@classmethod
584553
def _ensure_array(cls, data, dtype, copy: bool):
@@ -7081,19 +7050,6 @@ def shape(self) -> Shape:
70817050
# See GH#27775, GH#27384 for history/reasoning in how this is defined.
70827051
return (len(self),)
70837052

7084-
@final
7085-
def _deprecated_arg(self, value, name: str_t, methodname: str_t) -> None:
7086-
"""
7087-
Issue a FutureWarning if the arg/kwarg is not no_default.
7088-
"""
7089-
if value is not no_default:
7090-
warnings.warn(
7091-
f"'{name}' argument in {methodname} is deprecated "
7092-
"and will be removed in a future version. Do not pass it.",
7093-
FutureWarning,
7094-
stacklevel=find_stack_level(),
7095-
)
7096-
70977053

70987054
def ensure_index_from_sequences(sequences, names=None) -> Index:
70997055
"""
@@ -7246,14 +7202,6 @@ def maybe_extract_name(name, obj, cls) -> Hashable:
72467202
return name
72477203

72487204

7249-
_cast_depr_msg = (
7250-
"In a future version, passing an object-dtype arraylike to pd.Index will "
7251-
"not infer numeric values to numeric dtype (matching the Series behavior). "
7252-
"To retain the old behavior, explicitly pass the desired dtype or use the "
7253-
"desired Index subclass"
7254-
)
7255-
7256-
72577205
def _maybe_cast_data_without_dtype(
72587206
subarr: np.ndarray, cast_numeric_deprecated: bool = True
72597207
) -> ArrayLike:

pandas/tests/indexes/base_class/test_constructors.py

-6
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ def test_construction_list_mixed_tuples(self, index_vals):
3030
assert isinstance(index, Index)
3131
assert not isinstance(index, MultiIndex)
3232

33-
def test_constructor_wrong_kwargs(self):
34-
# GH #19348
35-
with pytest.raises(TypeError, match="Unexpected keyword arguments {'foo'}"):
36-
with tm.assert_produces_warning(FutureWarning):
37-
Index([], foo="bar")
38-
3933
def test_constructor_cast(self):
4034
msg = "could not convert string to float"
4135
with pytest.raises(ValueError, match=msg):

pandas/tests/indexes/categorical/test_constructors.py

-10
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,5 @@ def test_construction_with_categorical_dtype(self):
145145
with pytest.raises(ValueError, match=msg):
146146
CategoricalIndex(data, categories=cats, dtype=dtype)
147147

148-
with pytest.raises(ValueError, match=msg):
149-
with tm.assert_produces_warning(FutureWarning):
150-
# passing subclass-specific kwargs to pd.Index
151-
Index(data, categories=cats, dtype=dtype)
152-
153148
with pytest.raises(ValueError, match=msg):
154149
CategoricalIndex(data, ordered=ordered, dtype=dtype)
155-
156-
with pytest.raises(ValueError, match=msg):
157-
with tm.assert_produces_warning(FutureWarning):
158-
# passing subclass-specific kwargs to pd.Index
159-
Index(data, ordered=ordered, dtype=dtype)

pandas/tests/indexes/datetimes/test_constructors.py

-27
Original file line numberDiff line numberDiff line change
@@ -409,17 +409,6 @@ def test_construction_index_with_mixed_timezones_with_NaT(self):
409409
assert isinstance(result, DatetimeIndex)
410410
assert result.tz is None
411411

412-
# all NaT with tz
413-
with tm.assert_produces_warning(FutureWarning):
414-
# subclass-specific kwargs to pd.Index
415-
result = Index([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx")
416-
exp = DatetimeIndex([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx")
417-
418-
tm.assert_index_equal(result, exp, exact=True)
419-
assert isinstance(result, DatetimeIndex)
420-
assert result.tz is not None
421-
assert result.tz == exp.tz
422-
423412
def test_construction_dti_with_mixed_timezones(self):
424413
# GH 11488 (not changed, added explicit tests)
425414

@@ -497,22 +486,6 @@ def test_construction_dti_with_mixed_timezones(self):
497486
name="idx",
498487
)
499488

500-
with pytest.raises(ValueError, match=msg):
501-
# passing tz should results in DatetimeIndex, then mismatch raises
502-
# TypeError
503-
with tm.assert_produces_warning(FutureWarning):
504-
# subclass-specific kwargs to pd.Index
505-
Index(
506-
[
507-
pd.NaT,
508-
Timestamp("2011-01-01 10:00"),
509-
pd.NaT,
510-
Timestamp("2011-01-02 10:00", tz="US/Eastern"),
511-
],
512-
tz="Asia/Tokyo",
513-
name="idx",
514-
)
515-
516489
def test_construction_base_constructor(self):
517490
arr = [Timestamp("2011-01-01"), pd.NaT, Timestamp("2011-01-03")]
518491
tm.assert_index_equal(Index(arr), DatetimeIndex(arr))

pandas/tests/indexes/interval/test_constructors.py

+11-15
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class ConstructorTests:
3838
get_kwargs_from_breaks to the expected format.
3939
"""
4040

41-
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
4241
@pytest.mark.parametrize(
4342
"breaks",
4443
[
@@ -96,22 +95,16 @@ def test_constructor_dtype(self, constructor, breaks, subtype):
9695
)
9796
def test_constructor_pass_closed(self, constructor, breaks):
9897
# not passing closed to IntervalDtype, but to IntervalArray constructor
99-
warn = None
100-
if isinstance(constructor, partial) and constructor.func is Index:
101-
# passing kwargs to Index is deprecated
102-
warn = FutureWarning
103-
10498
iv_dtype = IntervalDtype(breaks.dtype)
10599

106100
result_kwargs = self.get_kwargs_from_breaks(breaks)
107101

108102
for dtype in (iv_dtype, str(iv_dtype)):
109-
with tm.assert_produces_warning(warn):
103+
with tm.assert_produces_warning(None):
110104

111105
result = constructor(dtype=dtype, closed="left", **result_kwargs)
112106
assert result.dtype.closed == "left"
113107

114-
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
115108
@pytest.mark.parametrize("breaks", [[np.nan] * 2, [np.nan] * 4, [np.nan] * 50])
116109
def test_constructor_nan(self, constructor, breaks, closed):
117110
# GH 18421
@@ -125,7 +118,6 @@ def test_constructor_nan(self, constructor, breaks, closed):
125118
assert result.dtype.subtype == expected_subtype
126119
tm.assert_numpy_array_equal(np.array(result), expected_values)
127120

128-
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
129121
@pytest.mark.parametrize(
130122
"breaks",
131123
[
@@ -353,9 +345,14 @@ class TestClassConstructors(ConstructorTests):
353345
params=[IntervalIndex, partial(Index, dtype="interval")],
354346
ids=["IntervalIndex", "Index"],
355347
)
356-
def constructor(self, request):
348+
def klass(self, request):
349+
# We use a separate fixture here to include Index.__new__ with dtype kwarg
357350
return request.param
358351

352+
@pytest.fixture
353+
def constructor(self):
354+
return IntervalIndex
355+
359356
def get_kwargs_from_breaks(self, breaks, closed="right"):
360357
"""
361358
converts intervals in breaks format to a dictionary of kwargs to
@@ -388,27 +385,26 @@ def test_constructor_string(self):
388385
# the interval of strings is already forbidden.
389386
pass
390387

391-
def test_constructor_errors(self, constructor):
388+
def test_constructor_errors(self, klass):
392389
# mismatched closed within intervals with no constructor override
393390
ivs = [Interval(0, 1, closed="right"), Interval(2, 3, closed="left")]
394391
msg = "intervals must all be closed on the same side"
395392
with pytest.raises(ValueError, match=msg):
396-
constructor(ivs)
393+
klass(ivs)
397394

398395
# scalar
399396
msg = (
400397
r"IntervalIndex\(...\) must be called with a collection of "
401398
"some kind, 5 was passed"
402399
)
403400
with pytest.raises(TypeError, match=msg):
404-
constructor(5)
401+
klass(5)
405402

406403
# not an interval; dtype depends on 32bit/windows builds
407404
msg = "type <class 'numpy.int(32|64)'> with value 0 is not an interval"
408405
with pytest.raises(TypeError, match=msg):
409-
constructor([0, 1])
406+
klass([0, 1])
410407

411-
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
412408
@pytest.mark.parametrize(
413409
"data, closed",
414410
[

pandas/tests/indexes/multi/test_equivalence.py

-11
Original file line numberDiff line numberDiff line change
@@ -191,18 +191,7 @@ def test_identical(idx):
191191
mi2 = mi2.set_names(["new1", "new2"])
192192
assert mi.identical(mi2)
193193

194-
with tm.assert_produces_warning(FutureWarning):
195-
# subclass-specific keywords to pd.Index
196-
mi3 = Index(mi.tolist(), names=mi.names)
197-
198-
msg = r"Unexpected keyword arguments {'names'}"
199-
with pytest.raises(TypeError, match=msg):
200-
with tm.assert_produces_warning(FutureWarning):
201-
# subclass-specific keywords to pd.Index
202-
Index(mi.tolist(), names=mi.names, tupleize_cols=False)
203-
204194
mi4 = Index(mi.tolist(), tupleize_cols=False)
205-
assert mi.identical(mi3)
206195
assert not mi.identical(mi4)
207196
assert mi.equals(mi4)
208197

pandas/tests/indexes/multi/test_names.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ def test_take_preserve_name(idx):
5656
def test_copy_names():
5757
# Check that adding a "names" parameter to the copy is honored
5858
# GH14302
59-
with tm.assert_produces_warning(FutureWarning):
60-
# subclass-specific kwargs to pd.Index
61-
multi_idx = pd.Index([(1, 2), (3, 4)], names=["MyName1", "MyName2"])
59+
multi_idx = MultiIndex.from_tuples([(1, 2), (3, 4)], names=["MyName1", "MyName2"])
6260
multi_idx1 = multi_idx.copy()
6361

6462
assert multi_idx.equals(multi_idx1)

pandas/tests/indexes/test_base.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,13 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
250250

251251
if attr == "asi8":
252252
result = DatetimeIndex(arg).tz_localize(tz_naive_fixture)
253+
tm.assert_index_equal(result, index)
254+
elif klass is Index:
255+
with pytest.raises(TypeError, match="unexpected keyword"):
256+
klass(arg, tz=tz_naive_fixture)
253257
else:
254258
result = klass(arg, tz=tz_naive_fixture)
255-
tm.assert_index_equal(result, index)
259+
tm.assert_index_equal(result, index)
256260

257261
if attr == "asi8":
258262
if err:
@@ -267,9 +271,13 @@ def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
267271

268272
if attr == "asi8":
269273
result = DatetimeIndex(list(arg)).tz_localize(tz_naive_fixture)
274+
tm.assert_index_equal(result, index)
275+
elif klass is Index:
276+
with pytest.raises(TypeError, match="unexpected keyword"):
277+
klass(arg, tz=tz_naive_fixture)
270278
else:
271279
result = klass(list(arg), tz=tz_naive_fixture)
272-
tm.assert_index_equal(result, index)
280+
tm.assert_index_equal(result, index)
273281

274282
if attr == "asi8":
275283
if err:

pandas/tests/indexing/test_coercion.py

-6
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,6 @@ def test_insert_index_period(self, insert, coerced_val, coerced_dtype):
368368
expected = obj.astype(object).insert(0, str(insert))
369369
tm.assert_index_equal(result, expected)
370370

371-
msg = r"Unexpected keyword arguments {'freq'}"
372-
with pytest.raises(TypeError, match=msg):
373-
with tm.assert_produces_warning(FutureWarning):
374-
# passing keywords to pd.Index
375-
pd.Index(data, freq="M")
376-
377371
@pytest.mark.xfail(reason="Test not implemented")
378372
def test_insert_index_complex128(self):
379373
raise NotImplementedError

0 commit comments

Comments
 (0)