Skip to content

Commit 1bac7ac

Browse files
authored
DEPR: allowing subclass-specific keywords in pd.Index.__new__ (#38597)
1 parent ad4850b commit 1bac7ac

17 files changed

+112
-61
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Other API changes
143143

144144
Deprecations
145145
~~~~~~~~~~~~
146-
146+
- Deprecated allowing subclass-specific keyword arguments in the :class:`Index` constructor, use the specific subclass directly instead (:issue:`14093`,:issue:`21311`,:issue:`22315`,:issue:`26974`)
147147
-
148148
-
149149

pandas/core/indexes/base.py

+9
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,15 @@ def __new__(
252252
cls, data=None, dtype=None, copy=False, name=None, tupleize_cols=True, **kwargs
253253
) -> "Index":
254254

255+
if kwargs:
256+
warnings.warn(
257+
"Passing keywords other than 'data', 'dtype', 'copy', 'name', "
258+
"'tupleize_cols' is deprecated and will raise TypeError in a "
259+
"future version. Use the specific Index subclass directly instead",
260+
FutureWarning,
261+
stacklevel=2,
262+
)
263+
255264
from pandas.core.indexes.range import RangeIndex
256265

257266
name = maybe_extract_name(name, data, cls)

pandas/core/indexes/datetimelike.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def __array_wrap__(self, result, context=None):
147147
if not is_period_dtype(self.dtype) and attrs["freq"]:
148148
# no need to infer if freq is None
149149
attrs["freq"] = "infer"
150-
return Index(result, **attrs)
150+
return type(self)(result, **attrs)
151151

152152
# ------------------------------------------------------------------------
153153

pandas/core/resample.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1966,6 +1966,10 @@ def _asfreq_compat(index, freq):
19661966
new_index: Index
19671967
if isinstance(index, PeriodIndex):
19681968
new_index = index.asfreq(freq=freq)
1969-
else:
1970-
new_index = Index([], dtype=index.dtype, freq=freq, name=index.name)
1969+
elif isinstance(index, DatetimeIndex):
1970+
new_index = DatetimeIndex([], dtype=index.dtype, freq=freq, name=index.name)
1971+
elif isinstance(index, TimedeltaIndex):
1972+
new_index = TimedeltaIndex([], dtype=index.dtype, freq=freq, name=index.name)
1973+
else: # pragma: no cover
1974+
raise TypeError(type(index))
19711975
return new_index

pandas/io/pytables.py

+39-24
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import (
1313
TYPE_CHECKING,
1414
Any,
15+
Callable,
1516
Dict,
1617
List,
1718
Optional,
@@ -2045,15 +2046,19 @@ def convert(self, values: np.ndarray, nan_rep, encoding: str, errors: str):
20452046
if self.freq is not None:
20462047
kwargs["freq"] = _ensure_decoded(self.freq)
20472048

2049+
factory: Union[Type[Index], Type[DatetimeIndex]] = Index
2050+
if is_datetime64_dtype(values.dtype) or is_datetime64tz_dtype(values.dtype):
2051+
factory = DatetimeIndex
2052+
20482053
# making an Index instance could throw a number of different errors
20492054
try:
2050-
new_pd_index = Index(values, **kwargs)
2055+
new_pd_index = factory(values, **kwargs)
20512056
except ValueError:
20522057
# if the output freq is different that what we recorded,
20532058
# it should be None (see also 'doc example part 2')
20542059
if "freq" in kwargs:
20552060
kwargs["freq"] = None
2056-
new_pd_index = Index(values, **kwargs)
2061+
new_pd_index = factory(values, **kwargs)
20572062

20582063
new_pd_index = _set_tz(new_pd_index, self.tz)
20592064
return new_pd_index, new_pd_index
@@ -2736,8 +2741,14 @@ def _alias_to_class(self, alias):
27362741
return alias
27372742
return self._reverse_index_map.get(alias, Index)
27382743

2739-
def _get_index_factory(self, klass):
2740-
if klass == DatetimeIndex:
2744+
def _get_index_factory(self, attrs):
2745+
index_class = self._alias_to_class(
2746+
_ensure_decoded(getattr(attrs, "index_class", ""))
2747+
)
2748+
2749+
factory: Callable
2750+
2751+
if index_class == DatetimeIndex:
27412752

27422753
def f(values, freq=None, tz=None):
27432754
# data are already in UTC, localize and convert if tz present
@@ -2747,16 +2758,34 @@ def f(values, freq=None, tz=None):
27472758
result = result.tz_localize("UTC").tz_convert(tz)
27482759
return result
27492760

2750-
return f
2751-
elif klass == PeriodIndex:
2761+
factory = f
2762+
elif index_class == PeriodIndex:
27522763

27532764
def f(values, freq=None, tz=None):
27542765
parr = PeriodArray._simple_new(values, freq=freq)
27552766
return PeriodIndex._simple_new(parr, name=None)
27562767

2757-
return f
2768+
factory = f
2769+
else:
2770+
factory = index_class
2771+
2772+
kwargs = {}
2773+
if "freq" in attrs:
2774+
kwargs["freq"] = attrs["freq"]
2775+
if index_class is Index:
2776+
# DTI/PI would be gotten by _alias_to_class
2777+
factory = TimedeltaIndex
2778+
2779+
if "tz" in attrs:
2780+
if isinstance(attrs["tz"], bytes):
2781+
# created by python2
2782+
kwargs["tz"] = attrs["tz"].decode("utf-8")
2783+
else:
2784+
# created by python3
2785+
kwargs["tz"] = attrs["tz"]
2786+
assert index_class is DatetimeIndex # just checking
27582787

2759-
return klass
2788+
return factory, kwargs
27602789

27612790
def validate_read(self, columns, where):
27622791
"""
@@ -2928,22 +2957,8 @@ def read_index_node(
29282957
name = _ensure_str(node._v_attrs.name)
29292958
name = _ensure_decoded(name)
29302959

2931-
index_class = self._alias_to_class(
2932-
_ensure_decoded(getattr(node._v_attrs, "index_class", ""))
2933-
)
2934-
factory = self._get_index_factory(index_class)
2935-
2936-
kwargs = {}
2937-
if "freq" in node._v_attrs:
2938-
kwargs["freq"] = node._v_attrs["freq"]
2939-
2940-
if "tz" in node._v_attrs:
2941-
if isinstance(node._v_attrs["tz"], bytes):
2942-
# created by python2
2943-
kwargs["tz"] = node._v_attrs["tz"].decode("utf-8")
2944-
else:
2945-
# created by python3
2946-
kwargs["tz"] = node._v_attrs["tz"]
2960+
attrs = node._v_attrs
2961+
factory, kwargs = self._get_index_factory(attrs)
29472962

29482963
if kind == "date":
29492964
index = factory(

pandas/tests/groupby/test_grouping.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ def test_grouper_creation_bug(self):
254254
)
255255
result = s.groupby(pd.Grouper(level="three", freq="M")).sum()
256256
expected = Series(
257-
[28], index=Index([Timestamp("2013-01-31")], freq="M", name="three")
257+
[28],
258+
index=pd.DatetimeIndex([Timestamp("2013-01-31")], freq="M", name="three"),
258259
)
259260
tm.assert_series_equal(result, expected)
260261

pandas/tests/groupby/test_quantile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def test_columns_groupby_quantile():
271271
[9.6, 8.4, 10.6, 9.4],
272272
],
273273
index=list("XYZ"),
274-
columns=Index(
274+
columns=pd.MultiIndex.from_tuples(
275275
[("A", 0.8), ("A", 0.2), ("B", 0.8), ("B", 0.2)], names=["col", None]
276276
),
277277
)

pandas/tests/indexes/base_class/test_constructors.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33

44
from pandas import Index, MultiIndex
5+
import pandas._testing as tm
56

67

78
class TestIndexConstructor:
@@ -29,7 +30,8 @@ def test_construction_list_mixed_tuples(self, index_vals):
2930
def test_constructor_wrong_kwargs(self):
3031
# GH #19348
3132
with pytest.raises(TypeError, match="Unexpected keyword arguments {'foo'}"):
32-
Index([], foo="bar")
33+
with tm.assert_produces_warning(FutureWarning):
34+
Index([], foo="bar")
3335

3436
@pytest.mark.xfail(reason="see GH#21311: Index doesn't enforce dtype argument")
3537
def test_constructor_cast(self):

pandas/tests/indexes/categorical/test_constructors.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,14 @@ def test_construction_with_categorical_dtype(self):
129129
CategoricalIndex(data, categories=cats, dtype=dtype)
130130

131131
with pytest.raises(ValueError, match=msg):
132-
Index(data, categories=cats, dtype=dtype)
132+
with tm.assert_produces_warning(FutureWarning):
133+
# passing subclass-specific kwargs to pd.Index
134+
Index(data, categories=cats, dtype=dtype)
133135

134136
with pytest.raises(ValueError, match=msg):
135137
CategoricalIndex(data, ordered=ordered, dtype=dtype)
136138

137139
with pytest.raises(ValueError, match=msg):
138-
Index(data, ordered=ordered, dtype=dtype)
140+
with tm.assert_produces_warning(FutureWarning):
141+
# passing subclass-specific kwargs to pd.Index
142+
Index(data, ordered=ordered, dtype=dtype)

pandas/tests/indexes/datetimes/test_constructors.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ def test_construction_index_with_mixed_timezones_with_NaT(self):
374374
assert result.tz is None
375375

376376
# all NaT with tz
377-
result = Index([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx")
377+
with tm.assert_produces_warning(FutureWarning):
378+
# subclass-specific kwargs to pd.Index
379+
result = Index([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx")
378380
exp = DatetimeIndex([pd.NaT, pd.NaT], tz="Asia/Tokyo", name="idx")
379381

380382
tm.assert_index_equal(result, exp, exact=True)
@@ -462,16 +464,18 @@ def test_construction_dti_with_mixed_timezones(self):
462464
with pytest.raises(ValueError, match=msg):
463465
# passing tz should results in DatetimeIndex, then mismatch raises
464466
# TypeError
465-
Index(
466-
[
467-
pd.NaT,
468-
Timestamp("2011-01-01 10:00"),
469-
pd.NaT,
470-
Timestamp("2011-01-02 10:00", tz="US/Eastern"),
471-
],
472-
tz="Asia/Tokyo",
473-
name="idx",
474-
)
467+
with tm.assert_produces_warning(FutureWarning):
468+
# subclass-specific kwargs to pd.Index
469+
Index(
470+
[
471+
pd.NaT,
472+
Timestamp("2011-01-01 10:00"),
473+
pd.NaT,
474+
Timestamp("2011-01-02 10:00", tz="US/Eastern"),
475+
],
476+
tz="Asia/Tokyo",
477+
name="idx",
478+
)
475479

476480
def test_construction_base_constructor(self):
477481
arr = [Timestamp("2011-01-01"), pd.NaT, Timestamp("2011-01-03")]

pandas/tests/indexes/interval/test_constructors.py

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class ConstructorTests:
3636
get_kwargs_from_breaks to the expected format.
3737
"""
3838

39+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
3940
@pytest.mark.parametrize(
4041
"breaks",
4142
[
@@ -80,6 +81,7 @@ def test_constructor_dtype(self, constructor, breaks, subtype):
8081
result = constructor(dtype=dtype, **result_kwargs)
8182
tm.assert_index_equal(result, expected)
8283

84+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
8385
@pytest.mark.parametrize("breaks", [[np.nan] * 2, [np.nan] * 4, [np.nan] * 50])
8486
def test_constructor_nan(self, constructor, breaks, closed):
8587
# GH 18421
@@ -93,6 +95,7 @@ def test_constructor_nan(self, constructor, breaks, closed):
9395
assert result.dtype.subtype == expected_subtype
9496
tm.assert_numpy_array_equal(np.array(result), expected_values)
9597

98+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
9699
@pytest.mark.parametrize(
97100
"breaks",
98101
[
@@ -378,6 +381,7 @@ def test_constructor_errors(self, constructor):
378381
with pytest.raises(TypeError, match=msg):
379382
constructor([0, 1])
380383

384+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
381385
@pytest.mark.parametrize(
382386
"data, closed",
383387
[

pandas/tests/indexes/multi/test_equivalence.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,16 @@ def test_identical(idx):
185185
mi2 = mi2.set_names(["new1", "new2"])
186186
assert mi.identical(mi2)
187187

188-
mi3 = Index(mi.tolist(), names=mi.names)
188+
with tm.assert_produces_warning(FutureWarning):
189+
# subclass-specific keywords to pd.Index
190+
mi3 = Index(mi.tolist(), names=mi.names)
191+
189192
msg = r"Unexpected keyword arguments {'names'}"
190193
with pytest.raises(TypeError, match=msg):
191-
Index(mi.tolist(), names=mi.names, tupleize_cols=False)
194+
with tm.assert_produces_warning(FutureWarning):
195+
# subclass-specific keywords to pd.Index
196+
Index(mi.tolist(), names=mi.names, tupleize_cols=False)
197+
192198
mi4 = Index(mi.tolist(), tupleize_cols=False)
193199
assert mi.identical(mi3)
194200
assert not mi.identical(mi4)

pandas/tests/indexes/multi/test_names.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ 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-
multi_idx = pd.Index([(1, 2), (3, 4)], names=["MyName1", "MyName2"])
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"])
6062
multi_idx1 = multi_idx.copy()
6163

6264
assert multi_idx.equals(multi_idx1)

pandas/tests/indexes/test_base.py

+2
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ def test_constructor_dtypes_to_timedelta(self, cast_index, vals):
335335
index = Index(vals)
336336
assert isinstance(index, TimedeltaIndex)
337337

338+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
338339
@pytest.mark.parametrize("attr", ["values", "asi8"])
339340
@pytest.mark.parametrize("klass", [Index, DatetimeIndex])
340341
def test_constructor_dtypes_datetime(self, tz_naive_fixture, attr, klass):
@@ -2255,6 +2256,7 @@ def test_index_subclass_constructor_wrong_kwargs(index_maker):
22552256
index_maker(foo="bar")
22562257

22572258

2259+
@pytest.mark.filterwarnings("ignore:Passing keywords other:FutureWarning")
22582260
def test_deprecated_fastpath():
22592261
msg = "[Uu]nexpected keyword argument"
22602262
with pytest.raises(TypeError, match=msg):

pandas/tests/indexes/test_numeric.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def test_index_groupby(self):
5757
idx.groupby(to_groupby), {1.0: idx[[0, 5]], 2.0: idx[[1, 4]]}
5858
)
5959

60-
to_groupby = Index(
60+
to_groupby = pd.DatetimeIndex(
6161
[
6262
datetime(2011, 11, 1),
6363
datetime(2011, 12, 1),

pandas/tests/indexing/test_coercion.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,9 @@ def test_insert_index_period(self, insert, coerced_val, coerced_dtype):
506506
else:
507507
msg = r"Unexpected keyword arguments {'freq'}"
508508
with pytest.raises(TypeError, match=msg):
509-
pd.Index(data, freq="M")
509+
with tm.assert_produces_warning(FutureWarning):
510+
# passing keywords to pd.Index
511+
pd.Index(data, freq="M")
510512

511513
def test_insert_index_complex128(self):
512514
pytest.xfail("Test not implemented")

pandas/tests/io/json/test_json_table_schema.py

+8-12
Original file line numberDiff line numberDiff line change
@@ -705,18 +705,14 @@ def test_read_json_table_orient_raises(self, index_nm, vals, recwarn):
705705
"idx",
706706
[
707707
pd.Index(range(4)),
708-
pd.Index(
709-
pd.date_range(
710-
"2020-08-30",
711-
freq="d",
712-
periods=4,
713-
),
714-
freq=None,
715-
),
716-
pd.Index(
717-
pd.date_range("2020-08-30", freq="d", periods=4, tz="US/Central"),
718-
freq=None,
719-
),
708+
pd.date_range(
709+
"2020-08-30",
710+
freq="d",
711+
periods=4,
712+
)._with_freq(None),
713+
pd.date_range(
714+
"2020-08-30", freq="d", periods=4, tz="US/Central"
715+
)._with_freq(None),
720716
pd.MultiIndex.from_product(
721717
[
722718
pd.date_range("2020-08-30", freq="d", periods=2, tz="US/Central"),

0 commit comments

Comments
 (0)