Skip to content

Commit 2e9e89a

Browse files
authored
API: Revert 57042 - MultiIndex.names|codes|levels returns tuples (#57788)
* API: Revert 57042 - MultiIndex.names|codes|levels returns tuples * Typing fixup * Docstring fixup * ruff
1 parent 8b6a82f commit 2e9e89a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+473
-228
lines changed

doc/source/whatsnew/v3.0.0.rst

-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for mor
147147
Other API changes
148148
^^^^^^^^^^^^^^^^^
149149
- 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`)
150-
- :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`)
151150
- :func:`read_table`'s ``parse_dates`` argument defaults to ``None`` to improve consistency with :func:`read_csv` (:issue:`57476`)
152151
- Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`)
153152
- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. Custom styling can still be done using :meth:`Styler.to_excel` (:issue:`54154`)

pandas/_libs/index.pyi

+3-3
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ class MaskedUInt8Engine(MaskedIndexEngine): ...
7373
class MaskedBoolEngine(MaskedUInt8Engine): ...
7474

7575
class BaseMultiIndexCodesEngine:
76-
levels: tuple[np.ndarray]
76+
levels: list[np.ndarray]
7777
offsets: np.ndarray # ndarray[uint64_t, ndim=1]
7878

7979
def __init__(
8080
self,
81-
levels: tuple[Index, ...], # all entries hashable
82-
labels: tuple[np.ndarray], # all entries integer-dtyped
81+
levels: list[Index], # all entries hashable
82+
labels: list[np.ndarray], # all entries integer-dtyped
8383
offsets: np.ndarray, # np.ndarray[np.uint64, ndim=1]
8484
) -> None: ...
8585
def get_indexer(self, target: npt.NDArray[np.object_]) -> npt.NDArray[np.intp]: ...

pandas/core/groupby/groupby.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5609,7 +5609,7 @@ def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiInde
56095609
idx = cast(MultiIndex, idx)
56105610
levels = list(idx.levels) + [lev]
56115611
codes = [np.repeat(x, nqs) for x in idx.codes] + [np.tile(lev_codes, len(idx))]
5612-
mi = MultiIndex(levels=levels, codes=codes, names=list(idx.names) + [None])
5612+
mi = MultiIndex(levels=levels, codes=codes, names=idx.names + [None])
56135613
else:
56145614
nidx = len(idx)
56155615
idx_codes = coerce_indexer_dtype(np.arange(nidx), idx)

pandas/core/indexes/base.py

+15-16
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
disallow_ndim_indexing,
175175
is_valid_positional_slice,
176176
)
177+
from pandas.core.indexes.frozen import FrozenList
177178
from pandas.core.missing import clean_reindex_fill_method
178179
from pandas.core.ops import get_op_result_name
179180
from pandas.core.sorting import (
@@ -1726,8 +1727,8 @@ def _get_default_index_names(
17261727

17271728
return names
17281729

1729-
def _get_names(self) -> tuple[Hashable | None, ...]:
1730-
return (self.name,)
1730+
def _get_names(self) -> FrozenList:
1731+
return FrozenList((self.name,))
17311732

17321733
def _set_names(self, values, *, level=None) -> None:
17331734
"""
@@ -1821,7 +1822,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
18211822
('python', 2019),
18221823
( 'cobra', 2018),
18231824
( 'cobra', 2019)],
1824-
names=('species', 'year'))
1825+
names=['species', 'year'])
18251826
18261827
When renaming levels with a dict, levels can not be passed.
18271828
@@ -1830,7 +1831,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
18301831
('python', 2019),
18311832
( 'cobra', 2018),
18321833
( 'cobra', 2019)],
1833-
names=('snake', 'year'))
1834+
names=['snake', 'year'])
18341835
"""
18351836
if level is not None and not isinstance(self, ABCMultiIndex):
18361837
raise ValueError("Level must be None for non-MultiIndex")
@@ -1915,13 +1916,13 @@ def rename(self, name, *, inplace: bool = False) -> Self | None:
19151916
('python', 2019),
19161917
( 'cobra', 2018),
19171918
( 'cobra', 2019)],
1918-
names=('kind', 'year'))
1919+
names=['kind', 'year'])
19191920
>>> idx.rename(["species", "year"])
19201921
MultiIndex([('python', 2018),
19211922
('python', 2019),
19221923
( 'cobra', 2018),
19231924
( 'cobra', 2019)],
1924-
names=('species', 'year'))
1925+
names=['species', 'year'])
19251926
>>> idx.rename("species")
19261927
Traceback (most recent call last):
19271928
TypeError: Must pass list-like as `names`.
@@ -2085,22 +2086,22 @@ def droplevel(self, level: IndexLabel = 0):
20852086
>>> mi
20862087
MultiIndex([(1, 3, 5),
20872088
(2, 4, 6)],
2088-
names=('x', 'y', 'z'))
2089+
names=['x', 'y', 'z'])
20892090
20902091
>>> mi.droplevel()
20912092
MultiIndex([(3, 5),
20922093
(4, 6)],
2093-
names=('y', 'z'))
2094+
names=['y', 'z'])
20942095
20952096
>>> mi.droplevel(2)
20962097
MultiIndex([(1, 3),
20972098
(2, 4)],
2098-
names=('x', 'y'))
2099+
names=['x', 'y'])
20992100
21002101
>>> mi.droplevel("z")
21012102
MultiIndex([(1, 3),
21022103
(2, 4)],
2103-
names=('x', 'y'))
2104+
names=['x', 'y'])
21042105
21052106
>>> mi.droplevel(["x", "y"])
21062107
Index([5, 6], dtype='int64', name='z')
@@ -4437,9 +4438,7 @@ def _join_level(
44374438
"""
44384439
from pandas.core.indexes.multi import MultiIndex
44394440

4440-
def _get_leaf_sorter(
4441-
labels: tuple[np.ndarray, ...] | list[np.ndarray],
4442-
) -> npt.NDArray[np.intp]:
4441+
def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
44434442
"""
44444443
Returns sorter for the inner most level while preserving the
44454444
order of higher levels.
@@ -6184,13 +6183,13 @@ def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
61846183
array([ True, False, False])
61856184
61866185
>>> midx = pd.MultiIndex.from_arrays(
6187-
... [[1, 2, 3], ["red", "blue", "green"]], names=("number", "color")
6186+
... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
61886187
... )
61896188
>>> midx
61906189
MultiIndex([(1, 'red'),
61916190
(2, 'blue'),
61926191
(3, 'green')],
6193-
names=('number', 'color'))
6192+
names=['number', 'color'])
61946193
61956194
Check whether the strings in the 'color' level of the MultiIndex
61966195
are in a list of colors.
@@ -7178,7 +7177,7 @@ def ensure_index_from_sequences(sequences, names=None) -> Index:
71787177
>>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
71797178
MultiIndex([('a', 'a'),
71807179
('a', 'b')],
7181-
names=('L1', 'L2'))
7180+
names=['L1', 'L2'])
71827181
71837182
See Also
71847183
--------

pandas/core/indexes/frozen.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""
2+
frozen (immutable) data structures to support MultiIndexing
3+
4+
These are used for:
5+
6+
- .names (FrozenList)
7+
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from typing import (
13+
TYPE_CHECKING,
14+
NoReturn,
15+
)
16+
17+
from pandas.core.base import PandasObject
18+
19+
from pandas.io.formats.printing import pprint_thing
20+
21+
if TYPE_CHECKING:
22+
from pandas._typing import Self
23+
24+
25+
class FrozenList(PandasObject, list):
26+
"""
27+
Container that doesn't allow setting item *but*
28+
because it's technically hashable, will be used
29+
for lookups, appropriately, etc.
30+
"""
31+
32+
# Side note: This has to be of type list. Otherwise,
33+
# it messes up PyTables type checks.
34+
35+
def union(self, other) -> FrozenList:
36+
"""
37+
Returns a FrozenList with other concatenated to the end of self.
38+
39+
Parameters
40+
----------
41+
other : array-like
42+
The array-like whose elements we are concatenating.
43+
44+
Returns
45+
-------
46+
FrozenList
47+
The collection difference between self and other.
48+
"""
49+
if isinstance(other, tuple):
50+
other = list(other)
51+
return type(self)(super().__add__(other))
52+
53+
def difference(self, other) -> FrozenList:
54+
"""
55+
Returns a FrozenList with elements from other removed from self.
56+
57+
Parameters
58+
----------
59+
other : array-like
60+
The array-like whose elements we are removing self.
61+
62+
Returns
63+
-------
64+
FrozenList
65+
The collection difference between self and other.
66+
"""
67+
other = set(other)
68+
temp = [x for x in self if x not in other]
69+
return type(self)(temp)
70+
71+
# TODO: Consider deprecating these in favor of `union` (xref gh-15506)
72+
# error: Incompatible types in assignment (expression has type
73+
# "Callable[[FrozenList, Any], FrozenList]", base class "list" defined the
74+
# type as overloaded function)
75+
__add__ = __iadd__ = union # type: ignore[assignment]
76+
77+
def __getitem__(self, n):
78+
if isinstance(n, slice):
79+
return type(self)(super().__getitem__(n))
80+
return super().__getitem__(n)
81+
82+
def __radd__(self, other) -> Self:
83+
if isinstance(other, tuple):
84+
other = list(other)
85+
return type(self)(other + list(self))
86+
87+
def __eq__(self, other: object) -> bool:
88+
if isinstance(other, (tuple, FrozenList)):
89+
other = list(other)
90+
return super().__eq__(other)
91+
92+
__req__ = __eq__
93+
94+
def __mul__(self, other) -> Self:
95+
return type(self)(super().__mul__(other))
96+
97+
__imul__ = __mul__
98+
99+
def __reduce__(self):
100+
return type(self), (list(self),)
101+
102+
# error: Signature of "__hash__" incompatible with supertype "list"
103+
def __hash__(self) -> int: # type: ignore[override]
104+
return hash(tuple(self))
105+
106+
def _disabled(self, *args, **kwargs) -> NoReturn:
107+
"""
108+
This method will not function because object is immutable.
109+
"""
110+
raise TypeError(f"'{type(self).__name__}' does not support mutable operations.")
111+
112+
def __str__(self) -> str:
113+
return pprint_thing(self, quote_strings=True, escape_chars=("\t", "\r", "\n"))
114+
115+
def __repr__(self) -> str:
116+
return f"{type(self).__name__}({self!s})"
117+
118+
__setitem__ = __setslice__ = _disabled # type: ignore[assignment]
119+
__delitem__ = __delslice__ = _disabled
120+
pop = append = extend = _disabled
121+
remove = sort = insert = _disabled # type: ignore[assignment]

0 commit comments

Comments
 (0)