Skip to content

Commit

Permalink
API: Revert 57042 - MultiIndex.names|codes|levels returns tuples (#57788
Browse files Browse the repository at this point in the history
)

* API: Revert 57042 - MultiIndex.names|codes|levels returns tuples

* Typing fixup

* Docstring fixup

* ruff
  • Loading branch information
rhshadrach authored Apr 11, 2024
1 parent 8b6a82f commit 2e9e89a
Show file tree
Hide file tree
Showing 48 changed files with 473 additions and 228 deletions.
1 change: 0 additions & 1 deletion doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for mor
Other API changes
^^^^^^^^^^^^^^^^^
- 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`)
- :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`)
- :func:`read_table`'s ``parse_dates`` argument defaults to ``None`` to improve consistency with :func:`read_csv` (:issue:`57476`)
- Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`)
- 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`)
Expand Down
6 changes: 3 additions & 3 deletions pandas/_libs/index.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ class MaskedUInt8Engine(MaskedIndexEngine): ...
class MaskedBoolEngine(MaskedUInt8Engine): ...

class BaseMultiIndexCodesEngine:
levels: tuple[np.ndarray]
levels: list[np.ndarray]
offsets: np.ndarray # ndarray[uint64_t, ndim=1]

def __init__(
self,
levels: tuple[Index, ...], # all entries hashable
labels: tuple[np.ndarray], # all entries integer-dtyped
levels: list[Index], # all entries hashable
labels: list[np.ndarray], # all entries integer-dtyped
offsets: np.ndarray, # np.ndarray[np.uint64, ndim=1]
) -> None: ...
def get_indexer(self, target: npt.NDArray[np.object_]) -> npt.NDArray[np.intp]: ...
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/groupby/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -5609,7 +5609,7 @@ def _insert_quantile_level(idx: Index, qs: npt.NDArray[np.float64]) -> MultiInde
idx = cast(MultiIndex, idx)
levels = list(idx.levels) + [lev]
codes = [np.repeat(x, nqs) for x in idx.codes] + [np.tile(lev_codes, len(idx))]
mi = MultiIndex(levels=levels, codes=codes, names=list(idx.names) + [None])
mi = MultiIndex(levels=levels, codes=codes, names=idx.names + [None])
else:
nidx = len(idx)
idx_codes = coerce_indexer_dtype(np.arange(nidx), idx)
Expand Down
31 changes: 15 additions & 16 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
disallow_ndim_indexing,
is_valid_positional_slice,
)
from pandas.core.indexes.frozen import FrozenList
from pandas.core.missing import clean_reindex_fill_method
from pandas.core.ops import get_op_result_name
from pandas.core.sorting import (
Expand Down Expand Up @@ -1726,8 +1727,8 @@ def _get_default_index_names(

return names

def _get_names(self) -> tuple[Hashable | None, ...]:
return (self.name,)
def _get_names(self) -> FrozenList:
return FrozenList((self.name,))

def _set_names(self, values, *, level=None) -> None:
"""
Expand Down Expand Up @@ -1821,7 +1822,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('species', 'year'))
names=['species', 'year'])
When renaming levels with a dict, levels can not be passed.
Expand All @@ -1830,7 +1831,7 @@ def set_names(self, names, *, level=None, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('snake', 'year'))
names=['snake', 'year'])
"""
if level is not None and not isinstance(self, ABCMultiIndex):
raise ValueError("Level must be None for non-MultiIndex")
Expand Down Expand Up @@ -1915,13 +1916,13 @@ def rename(self, name, *, inplace: bool = False) -> Self | None:
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('kind', 'year'))
names=['kind', 'year'])
>>> idx.rename(["species", "year"])
MultiIndex([('python', 2018),
('python', 2019),
( 'cobra', 2018),
( 'cobra', 2019)],
names=('species', 'year'))
names=['species', 'year'])
>>> idx.rename("species")
Traceback (most recent call last):
TypeError: Must pass list-like as `names`.
Expand Down Expand Up @@ -2085,22 +2086,22 @@ def droplevel(self, level: IndexLabel = 0):
>>> mi
MultiIndex([(1, 3, 5),
(2, 4, 6)],
names=('x', 'y', 'z'))
names=['x', 'y', 'z'])
>>> mi.droplevel()
MultiIndex([(3, 5),
(4, 6)],
names=('y', 'z'))
names=['y', 'z'])
>>> mi.droplevel(2)
MultiIndex([(1, 3),
(2, 4)],
names=('x', 'y'))
names=['x', 'y'])
>>> mi.droplevel("z")
MultiIndex([(1, 3),
(2, 4)],
names=('x', 'y'))
names=['x', 'y'])
>>> mi.droplevel(["x", "y"])
Index([5, 6], dtype='int64', name='z')
Expand Down Expand Up @@ -4437,9 +4438,7 @@ def _join_level(
"""
from pandas.core.indexes.multi import MultiIndex

def _get_leaf_sorter(
labels: tuple[np.ndarray, ...] | list[np.ndarray],
) -> npt.NDArray[np.intp]:
def _get_leaf_sorter(labels: list[np.ndarray]) -> npt.NDArray[np.intp]:
"""
Returns sorter for the inner most level while preserving the
order of higher levels.
Expand Down Expand Up @@ -6184,13 +6183,13 @@ def isin(self, values, level=None) -> npt.NDArray[np.bool_]:
array([ True, False, False])
>>> midx = pd.MultiIndex.from_arrays(
... [[1, 2, 3], ["red", "blue", "green"]], names=("number", "color")
... [[1, 2, 3], ["red", "blue", "green"]], names=["number", "color"]
... )
>>> midx
MultiIndex([(1, 'red'),
(2, 'blue'),
(3, 'green')],
names=('number', 'color'))
names=['number', 'color'])
Check whether the strings in the 'color' level of the MultiIndex
are in a list of colors.
Expand Down Expand Up @@ -7178,7 +7177,7 @@ def ensure_index_from_sequences(sequences, names=None) -> Index:
>>> ensure_index_from_sequences([["a", "a"], ["a", "b"]], names=["L1", "L2"])
MultiIndex([('a', 'a'),
('a', 'b')],
names=('L1', 'L2'))
names=['L1', 'L2'])
See Also
--------
Expand Down
121 changes: 121 additions & 0 deletions pandas/core/indexes/frozen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
frozen (immutable) data structures to support MultiIndexing
These are used for:
- .names (FrozenList)
"""

from __future__ import annotations

from typing import (
TYPE_CHECKING,
NoReturn,
)

from pandas.core.base import PandasObject

from pandas.io.formats.printing import pprint_thing

if TYPE_CHECKING:
from pandas._typing import Self


class FrozenList(PandasObject, list):
"""
Container that doesn't allow setting item *but*
because it's technically hashable, will be used
for lookups, appropriately, etc.
"""

# Side note: This has to be of type list. Otherwise,
# it messes up PyTables type checks.

def union(self, other) -> FrozenList:
"""
Returns a FrozenList with other concatenated to the end of self.
Parameters
----------
other : array-like
The array-like whose elements we are concatenating.
Returns
-------
FrozenList
The collection difference between self and other.
"""
if isinstance(other, tuple):
other = list(other)
return type(self)(super().__add__(other))

def difference(self, other) -> FrozenList:
"""
Returns a FrozenList with elements from other removed from self.
Parameters
----------
other : array-like
The array-like whose elements we are removing self.
Returns
-------
FrozenList
The collection difference between self and other.
"""
other = set(other)
temp = [x for x in self if x not in other]
return type(self)(temp)

# TODO: Consider deprecating these in favor of `union` (xref gh-15506)
# error: Incompatible types in assignment (expression has type
# "Callable[[FrozenList, Any], FrozenList]", base class "list" defined the
# type as overloaded function)
__add__ = __iadd__ = union # type: ignore[assignment]

def __getitem__(self, n):
if isinstance(n, slice):
return type(self)(super().__getitem__(n))
return super().__getitem__(n)

def __radd__(self, other) -> Self:
if isinstance(other, tuple):
other = list(other)
return type(self)(other + list(self))

def __eq__(self, other: object) -> bool:
if isinstance(other, (tuple, FrozenList)):
other = list(other)
return super().__eq__(other)

__req__ = __eq__

def __mul__(self, other) -> Self:
return type(self)(super().__mul__(other))

__imul__ = __mul__

def __reduce__(self):
return type(self), (list(self),)

# error: Signature of "__hash__" incompatible with supertype "list"
def __hash__(self) -> int: # type: ignore[override]
return hash(tuple(self))

def _disabled(self, *args, **kwargs) -> NoReturn:
"""
This method will not function because object is immutable.
"""
raise TypeError(f"'{type(self).__name__}' does not support mutable operations.")

def __str__(self) -> str:
return pprint_thing(self, quote_strings=True, escape_chars=("\t", "\r", "\n"))

def __repr__(self) -> str:
return f"{type(self).__name__}({self!s})"

__setitem__ = __setslice__ = _disabled # type: ignore[assignment]
__delitem__ = __delslice__ = _disabled
pop = append = extend = _disabled
remove = sort = insert = _disabled # type: ignore[assignment]
Loading

0 comments on commit 2e9e89a

Please sign in to comment.