Skip to content

Commit 198a309

Browse files
committed
ENH: Add validation for positional levels and improve error messages in Index class
1 parent 19832f3 commit 198a309

File tree

2 files changed

+166
-36
lines changed

2 files changed

+166
-36
lines changed

pandas/core/indexes/base.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
is_datetime_array,
4040
no_default,
4141
)
42+
from pandas._libs.missing import is_matching_na
4243
from pandas._libs.tslibs import (
4344
OutOfBoundsDatetime,
4445
Timestamp,
@@ -2118,6 +2119,30 @@ def _sort_levels_monotonic(self) -> Self:
21182119
"""
21192120
return self
21202121

2122+
@final
2123+
def _validate_positional_level(self, level: int) -> None:
2124+
"""
2125+
Validate that level is 0 or -1 for single-level Index.
2126+
2127+
Parameters
2128+
----------
2129+
level : int
2130+
The positional level to validate.
2131+
2132+
Raises
2133+
------
2134+
IndexError
2135+
If level is not 0 or -1.
2136+
"""
2137+
if level < 0 and level != -1:
2138+
raise IndexError(
2139+
f"Too many levels: Index has only 1 level, not {level + 1}"
2140+
)
2141+
elif level > 0:
2142+
raise IndexError(
2143+
f"Too many levels: Index has only 1 level, not {level + 1}"
2144+
)
2145+
21212146
@final
21222147
def _validate_index_level(self, level) -> None:
21232148
"""
@@ -2127,56 +2152,49 @@ def _validate_index_level(self, level) -> None:
21272152
verification must be done like in MultiIndex.
21282153
21292154
"""
2130-
if isna(level) and isna(self.name):
2131-
return
2155+
level_is_na = isna(level)
2156+
name_is_na = isna(self.name)
2157+
2158+
if level_is_na and name_is_na:
2159+
if is_matching_na(level, self.name):
2160+
return
2161+
raise KeyError(
2162+
f"Requested level ({level}) does not match index name ({self.name})"
2163+
)
21322164

2133-
elif isna(level) and not isna(self.name):
2165+
if level_is_na:
21342166
raise KeyError(
21352167
f"Requested level ({level}) does not match index name ({self.name})"
21362168
)
21372169

2138-
elif not isna(level) and isna(self.name):
2139-
# level is not NA, but self.name is NA
2140-
# This is valid for integer levels (0, -1) accessing unnamed index
2170+
if name_is_na:
21412171
if lib.is_integer(level):
2142-
if level < 0 and level != -1:
2143-
raise IndexError(
2144-
f"Too many levels: Index has only 1 level, not {level + 1}"
2145-
)
2146-
elif level > 0:
2147-
raise IndexError(
2148-
f"Too many levels: Index has only 1 level, not {level + 1}"
2149-
)
2172+
self._validate_positional_level(level)
21502173
return
2151-
else:
2152-
raise KeyError(
2153-
f"Requested level ({level}) does not match index name ({self.name})"
2154-
)
2174+
raise KeyError(
2175+
f"Requested level ({level}) does not match index name ({self.name})"
2176+
)
2177+
2178+
if isinstance(level, bool) or isinstance(self.name, bool):
2179+
if level == self.name:
2180+
return
2181+
raise KeyError(
2182+
f"Requested level ({level}) does not match index name ({self.name})"
2183+
)
21552184

2156-
elif lib.is_integer(level):
2185+
if lib.is_integer(level):
21572186
if isinstance(self.name, int) and level == self.name:
21582187
return
2159-
if level < 0 and level != -1:
2160-
raise IndexError(
2161-
f"Too many levels: Index has only 1 level, not {level + 1}"
2162-
)
2163-
elif level > 0:
2164-
raise IndexError(
2165-
f"Too many levels: Index has only 1 level, not {level + 1}"
2166-
)
2188+
2189+
self._validate_positional_level(level)
21672190
return
21682191

2169-
elif isinstance(level, str) and isinstance(self.name, str):
2170-
if level != self.name:
2171-
raise KeyError(
2172-
f"Requested level ({level}) does not match index name ({self.name})"
2173-
)
2192+
if level == self.name:
21742193
return
21752194

2176-
elif level != self.name:
2177-
raise KeyError(
2178-
f"Requested level ({level}) does not match index name ({self.name})"
2179-
)
2195+
raise KeyError(
2196+
f"Requested level ({level}) does not match index name ({self.name})"
2197+
)
21802198

21812199
def _get_level_number(self, level) -> int:
21822200
self._validate_index_level(level)

pandas/tests/indexes/test_base.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,118 @@ def test_get_level_values(self, index, name, level):
962962
result = expected.get_level_values(level)
963963
tm.assert_index_equal(result, expected)
964964

965+
def test_get_level_values_boolean_name(self):
966+
# GH#62175
967+
idx = Index([1, 2, 3], name=True)
968+
result = idx.get_level_values(True)
969+
tm.assert_index_equal(result, idx)
970+
971+
idx = Index([1, 2, 3], name=False)
972+
result = idx.get_level_values(False)
973+
tm.assert_index_equal(result, idx)
974+
975+
idx = Index([1, 2, 3], name=True)
976+
msg = r"Requested level \(False\) does not match index name \(True\)"
977+
with pytest.raises(KeyError, match=msg):
978+
idx.get_level_values(False)
979+
980+
msg = r"Too many levels: Index has only 1 level"
981+
with pytest.raises(IndexError, match=msg):
982+
idx.get_level_values(1)
983+
984+
def test_get_level_values_na_types(self):
985+
# GH#62175
986+
idx = Index([1, 2, 3], name=pd.NA)
987+
result = idx.get_level_values(pd.NA)
988+
tm.assert_index_equal(result, idx)
989+
990+
msg = r"Requested level \(.+\) does not match index name \(.+\)"
991+
with pytest.raises(KeyError, match=msg):
992+
idx.get_level_values(np.nan)
993+
994+
with pytest.raises(KeyError, match=msg):
995+
idx.get_level_values(None)
996+
997+
idx = Index([1, 2, 3], name=np.nan)
998+
result = idx.get_level_values(np.nan)
999+
tm.assert_index_equal(result, idx)
1000+
1001+
with pytest.raises(KeyError, match=msg):
1002+
idx.get_level_values(pd.NA)
1003+
1004+
with pytest.raises(KeyError, match=msg):
1005+
idx.get_level_values(None)
1006+
1007+
idx = Index([1, 2, 3], name=None)
1008+
result = idx.get_level_values(0)
1009+
tm.assert_index_equal(result, idx)
1010+
1011+
result = idx.get_level_values(-1)
1012+
tm.assert_index_equal(result, idx)
1013+
1014+
with pytest.raises(KeyError, match=msg):
1015+
idx.get_level_values(pd.NA)
1016+
1017+
with pytest.raises(KeyError, match=msg):
1018+
idx.get_level_values(np.nan)
1019+
1020+
idx = Index([1, 2, 3], name=pd.NaT)
1021+
result = idx.get_level_values(pd.NaT)
1022+
tm.assert_index_equal(result, idx)
1023+
1024+
with pytest.raises(KeyError, match=msg):
1025+
idx.get_level_values(np.nan)
1026+
1027+
with pytest.raises(KeyError, match=msg):
1028+
idx.get_level_values(pd.NA)
1029+
1030+
def test_get_level_values_integer_name_vs_position(self):
1031+
# GH#62175
1032+
idx = Index([1, 2, 3], name=0)
1033+
result = idx.get_level_values(0)
1034+
tm.assert_index_equal(result, idx)
1035+
1036+
result = idx.get_level_values(-1)
1037+
tm.assert_index_equal(result, idx)
1038+
1039+
idx = Index([1, 2, 3], name=5)
1040+
result = idx.get_level_values(5)
1041+
tm.assert_index_equal(result, idx)
1042+
1043+
result = idx.get_level_values(0)
1044+
tm.assert_index_equal(result, idx)
1045+
1046+
msg = r"Too many levels: Index has only 1 level, not 2"
1047+
with pytest.raises(IndexError, match=msg):
1048+
idx.get_level_values(1)
1049+
1050+
msg = r"Too many levels: Index has only 1 level, not 1"
1051+
with pytest.raises(IndexError, match=msg):
1052+
idx.get_level_values(-2)
1053+
1054+
def test_get_level_values_error_messages(self):
1055+
# GH#62175
1056+
idx = Index([1, 2, 3], name="foo")
1057+
1058+
msg = r"Requested level \(bar\) does not match index name \(foo\)"
1059+
with pytest.raises(KeyError, match=msg):
1060+
idx.get_level_values("bar")
1061+
1062+
msg = r"Too many levels: Index has only 1 level, not 2"
1063+
with pytest.raises(IndexError, match=msg):
1064+
idx.get_level_values(1)
1065+
1066+
msg = r"Too many levels: Index has only 1 level, not 1"
1067+
with pytest.raises(IndexError, match=msg):
1068+
idx.get_level_values(-2)
1069+
1070+
msg = r"Requested level \(.+\) does not match index name \(foo\)"
1071+
with pytest.raises(KeyError, match=msg):
1072+
idx.get_level_values(pd.NA)
1073+
1074+
with pytest.raises(KeyError, match=msg):
1075+
idx.get_level_values(np.nan)
1076+
9651077
def test_slice_keep_name(self):
9661078
index = Index(["a", "b"], name="asdf")
9671079
assert index.name == index[1:].name

0 commit comments

Comments
 (0)