pandas-dev/pandas · #70915

BUG: MultiIndex.rename rejects the name keyword (GH#33778)

jbrockmendel · merged Oct 18, 20264 files · 115 + / 5 −
doc/source/whatsnew/v3.2.0.rst1 + / 0 −
@@ -218,6 +218,7 @@ MultiIndex - Bug in :meth:`DataFrame.sort_index`, :meth:`Series.sort_index`, :meth:`MultiIndex.sort_values` and :meth:`MultiIndex.is_monotonic_increasing` ignoring the category order of a :class:`CategoricalIndex` level (:issue:`47607`) - Bug in :meth:`MultiIndex.append`, :func:`concat`, :meth:`DataFrame.join` and :func:`merge` emitting a spurious ``RuntimeWarning`` when :class:`MultiIndex` level values could not be sorted (:issue:`61477`) - Bug in :meth:`MultiIndex.from_tuples` with an object ``ndarray``, :meth:`MultiIndex.append` and :meth:`Index.map` truncating or raising ``IndexError`` on tuples of different lengths; :meth:`MultiIndex.from_tuples` now raises a ``ValueError`` for an ``ndarray`` of tuples of different lengths (:issue:`50293`)+- Bug in :meth:`MultiIndex.rename` raising ``TypeError`` for the ``name`` keyword documented by :meth:`Index.rename` (:issue:`33778`) - Bug in :meth:`Series.map`, :meth:`Index.get_indexer`, :meth:`DataFrame.reindex` and :attr:`DataFrame.loc` with a :class:`MultiIndex` raising ``IndexError`` or matching on a prefix when looking up tuples whose length differs from the number of levels (:issue:`50293`) - 
pandas/core/indexes/multi.py90 + / 3 −
@@ -18,6 +18,7 @@     NoReturn,     Self,     cast,+    overload, ) import warnings @@ -5013,9 +5014,95 @@ def isin(self, values, level=None) -> npt.NDArray[np.bool_]:                 return np.zeros(len(levs), dtype=np.bool_)             return levs.isin(values) -    # error: Incompatible types in assignment (expression has type overloaded function,-    # base class "Index" defined the type as "Callable[[Index, Any, bool], Any]")-    rename = Index.set_names  # type: ignore[assignment]+    @overload+    def rename(+        self,+        name: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        *,+        names: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        level: Hashable | Sequence[Hashable] | None = ...,+        inplace: Literal[False] = ...,+    ) -> Self: ...++    @overload+    def rename(+        self,+        name: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        *,+        names: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        level: Hashable | Sequence[Hashable] | None = ...,+        inplace: Literal[True],+    ) -> None: ...++    @overload+    def rename(+        self,+        name: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        *,+        names: Hashable | Sequence[Hashable] | dict[Hashable, Hashable] = ...,+        level: Hashable | Sequence[Hashable] | None = ...,+        inplace: bool = ...,+    ) -> Self | None: ...++    def rename(+        self,+        name: Hashable+        | Sequence[Hashable]+        | dict[Hashable, Hashable]+        | lib.NoDefault = lib.no_default,+        *,+        names: Hashable+        | Sequence[Hashable]+        | dict[Hashable, Hashable]+        | lib.NoDefault = lib.no_default,+        level: Hashable | Sequence[Hashable] | None = None,+        inplace: bool = False,+    ) -> Self | None:+        """+        Alter MultiIndex level names.++        Equivalent to :meth:`MultiIndex.set_names`.++        Parameters+        ----------+        name : Hashable, sequence of Hashable, or dict-like+            Name(s) to set. A dict-like maps old level names to new ones.+        names : Hashable, sequence of Hashable, or dict-like+            Alias for ``name``; pass only one of the two.+        level : int, Hashable or a sequence of the previous, optional+            If ``name`` is not dict-like, level(s) to set (None for all levels).+        inplace : bool, default False+            Modifies the object directly, instead of creating a new MultiIndex.++        Returns+        -------+        MultiIndex or None+            The same type as the caller or None if ``inplace=True``.++        See Also+        --------+        MultiIndex.set_names : Set level names partially and by level.+        Index.rename : Alter Index name.++        Examples+        --------+        >>> mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"])+        >>> mi.rename(["a", "b"])+        MultiIndex([(1, 3),+                    (2, 4)],+                   names=['a', 'b'])+        >>> mi.rename("a", level=0)+        MultiIndex([(1, 3),+                    (2, 4)],+                   names=['a', 'y'])+        """+        if names is not lib.no_default:+            if name is not lib.no_default:+                raise TypeError("Pass only one of 'name' and 'names'.")+            name = names+        elif name is lib.no_default:+            raise TypeError("rename() missing required argument: 'name'")+        return self.set_names(name, level=level, inplace=inplace)      # ---------------------------------------------------------------     # Arithmetic/Numeric Methods - Disabled
pandas/tests/indexes/multi/test_get_set.py1 + / 2 −
@@ -90,10 +90,9 @@ def test_get_level_number_out_of_bounds(multiindex_dataframe_random_data):   def test_set_name_methods(idx):-    # so long as these are synonyms, we don't need to test set_names     index_names = ["first", "second"]-    assert idx.rename == idx.set_names     new_names = [name + "SUFFIX" for name in index_names]+    assert idx.rename(new_names).names == idx.set_names(new_names).names     ind = idx.set_names(new_names)     assert idx.names == index_names     assert ind.names == new_names
pandas/tests/indexes/multi/test_names.py23 + / 0 −
@@ -200,3 +200,26 @@ def test_multiindex_name_and_level_raising():     mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"])     with pytest.raises(TypeError, match="Can not pass level for dictlike `names`."):         mi.set_names(names={"x": "z"}, level={"x": "z"})+++@pytest.mark.parametrize("func", ["rename", "set_names"])+@pytest.mark.parametrize("arg", [False, 0])+def test_rename_positional_second_arg_raises(func, arg):+    # GH#33778 a positional second argument used to be read as `level`+    mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"])+    msg = "positional arguments but 3 were given"+    with pytest.raises(TypeError, match=msg):+        getattr(mi, func)(["foo", "bar"], arg)+++def test_rename_name_keyword():+    # GH#33778 the `name` keyword documented on Index.rename was rejected+    mi = pd.MultiIndex.from_arrays([[1, 2], [3, 4]], names=["x", "y"])+    expected = mi.set_names(["foo", "bar"])+    tm.assert_index_equal(mi.rename(name=["foo", "bar"]), expected)+    tm.assert_index_equal(mi.rename(names=["foo", "bar"]), expected)++    with pytest.raises(TypeError, match="Pass only one of 'name' and 'names'"):+        mi.rename(["foo", "bar"], names=["foo", "bar"])+    with pytest.raises(TypeError, match="missing required argument: 'name'"):+        mi.rename()