Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEPR: disallow subclass-specific keywords in Index.__new__ #49311

Merged
merged 2 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
62 changes: 5 additions & 57 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -437,29 +428,15 @@ 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

name = maybe_extract_name(name, data, cls)

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,
Expand All @@ -481,26 +458,24 @@ 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
elif dtype is not None:
# 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)

Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 0 additions & 6 deletions pandas/tests/indexes/base_class/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 0 additions & 10 deletions pandas/tests/indexes/categorical/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
27 changes: 0 additions & 27 deletions pandas/tests/indexes/datetimes/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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))
Expand Down
26 changes: 11 additions & 15 deletions pandas/tests/indexes/interval/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can just get rid of this line altogether now no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

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
Expand All @@ -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",
[
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -388,27 +385,26 @@ 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 = (
r"IntervalIndex\(...\) must be called with a collection of "
"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 <class 'numpy.int(32|64)'> 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",
[
Expand Down
11 changes: 0 additions & 11 deletions pandas/tests/indexes/multi/test_equivalence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 1 addition & 3 deletions pandas/tests/indexes/multi/test_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 10 additions & 2 deletions pandas/tests/indexes/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
6 changes: 0 additions & 6 deletions pandas/tests/indexing/test_coercion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down