diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 0f5b4a16d2f01..b4ed782d58890 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -264,6 +264,7 @@ Indexing - Bug in :meth:`DataFrame.iloc` where indexing a single row on a :class:`DataFrame` with a single ExtensionDtype column gave a copy instead of a view on the underlying data (:issue:`45241`) - Bug in setting a NA value (``None`` or ``np.nan``) into a :class:`Series` with int-based :class:`IntervalDtype` incorrectly casting to object dtype instead of a float-based :class:`IntervalDtype` (:issue:`45568`) - Bug in :meth:`Series.__setitem__` with a non-integer :class:`Index` when using an integer key to set a value that cannot be set inplace where a ``ValueError`` was raised insead of casting to a common dtype (:issue:`45070`) +- Bug in :meth:`Series.loc.__setitem__` and :meth:`Series.loc.__getitem__` not raising when using multiple keys without using a :class:`MultiIndex` (:issue:`13831`) - Bug when setting a value too large for a :class:`Series` dtype failing to coerce to a common type (:issue:`26049`, :issue:`32878`) - Bug in :meth:`loc.__setitem__` treating ``range`` keys as positional instead of label-based (:issue:`45479`) - Bug in :meth:`Series.__setitem__` when setting ``boolean`` dtype values containing ``NA`` incorrectly raising instead of casting to ``boolean`` dtype (:issue:`45462`) diff --git a/pandas/conftest.py b/pandas/conftest.py index 952177f342c46..ba90c9eedb53c 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1734,6 +1734,14 @@ def indexer_sli(request): return request.param +@pytest.fixture(params=[tm.loc, tm.iloc]) +def indexer_li(request): + """ + Parametrize over loc.__getitem__, iloc.__getitem__ + """ + return request.param + + @pytest.fixture(params=[tm.setitem, tm.iloc]) def indexer_si(request): """ diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 740228db711bc..62a5723feb3d5 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -934,7 +934,7 @@ def _getitem_nested_tuple(self, tup: tuple): # we are only getting non-hashable tuples, in particular ones # that themselves contain a slice entry # See test_loc_series_getitem_too_many_dimensions - raise ValueError("Too many indices") + raise IndexingError("Too many indexers") # this is a series with a multi-index specified a tuple of # selectors @@ -1260,6 +1260,14 @@ def _convert_to_indexer(self, key, axis: int): is_int_index = labels.is_integer() is_int_positional = is_integer(key) and not is_int_index + if ( + isinstance(key, tuple) + and not isinstance(labels, MultiIndex) + and self.ndim < 2 + and len(key) > 1 + ): + raise IndexingError("Too many indexers") + if is_scalar(key) or (isinstance(labels, MultiIndex) and is_hashable(key)): # Otherwise get_loc will raise InvalidIndexError @@ -1291,7 +1299,7 @@ def _convert_to_indexer(self, key, axis: int): if is_nested_tuple(key, labels): if self.ndim == 1 and any(isinstance(k, tuple) for k in key): # GH#35349 Raise if tuple in tuple for series - raise ValueError("Too many indices") + raise IndexingError("Too many indexers") return labels.get_locs(key) elif is_list_like_indexer(key): diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 6e1c3686f5dbe..ec44511fdffec 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -24,6 +24,7 @@ ) import pandas._testing as tm from pandas.core.api import Float64Index +from pandas.core.indexing import IndexingError from pandas.tests.indexing.common import _mklbl from pandas.tests.indexing.test_floats import gen_obj @@ -981,3 +982,31 @@ def test_extension_array_cross_section_converts(): result = df.iloc[0] tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + "ser, keys", + [(Series([10]), (0, 0)), (Series([1, 2, 3], index=list("abc")), (0, 1))], +) +def test_ser_tup_indexer_exceeds_dimensions(ser, keys, indexer_li): + # GH#13831 + exp_err, exp_msg = IndexingError, "Too many indexers" + with pytest.raises(exp_err, match=exp_msg): + indexer_li(ser)[keys] + + if indexer_li == tm.iloc: + # For iloc.__setitem__ we let numpy handle the error reporting. + exp_err, exp_msg = IndexError, "too many indices for array" + + with pytest.raises(exp_err, match=exp_msg): + indexer_li(ser)[keys] = 0 + + +def test_ser_list_indexer_exceeds_dimensions(indexer_li): + # GH#13831 + # Make sure an exception is raised when a tuple exceeds the dimension of the series, + # but not list when a list is used. + ser = Series([10]) + res = indexer_li(ser)[[0, 0]] + exp = Series([10, 10], index=Index([0, 0])) + tm.assert_series_equal(res, exp) diff --git a/pandas/tests/indexing/test_loc.py b/pandas/tests/indexing/test_loc.py index 46c0271ebf974..a1b18727ed182 100644 --- a/pandas/tests/indexing/test_loc.py +++ b/pandas/tests/indexing/test_loc.py @@ -2704,6 +2704,18 @@ def test_loc_with_period_index_indexer(): tm.assert_frame_equal(df, df.loc[list(idx)]) +def test_loc_setitem_multiindex_timestamp(): + # GH#13831 + vals = np.random.randn(8, 6) + idx = date_range("1/1/2000", periods=8) + cols = ["A", "B", "C", "D", "E", "F"] + exp = DataFrame(vals, index=idx, columns=cols) + exp.loc[exp.index[1], ("A", "B")] = np.nan + vals[1][0:2] = np.nan + res = DataFrame(vals, index=idx, columns=cols) + tm.assert_frame_equal(res, exp) + + def test_loc_getitem_multiindex_tuple_level(): # GH#27591 lev1 = ["a", "b", "c"] @@ -2959,11 +2971,11 @@ def test_loc_series_getitem_too_many_dimensions(self, indexer): index=MultiIndex.from_tuples([("A", "0"), ("A", "1"), ("B", "0")]), data=[21, 22, 23], ) - msg = "Too many indices" - with pytest.raises(ValueError, match=msg): + msg = "Too many indexers" + with pytest.raises(IndexingError, match=msg): ser.loc[indexer, :] - with pytest.raises(ValueError, match=msg): + with pytest.raises(IndexingError, match=msg): ser.loc[indexer, :] = 1 def test_loc_setitem(self, string_series):