From 2dd305c9f83beef57e4b7590409f29966a3be42a Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 30 Dec 2019 16:47:50 -0600 Subject: [PATCH] API: Raise when setting via name Closes https://github.com/pandas-dev/pandas/issues/29032 --- doc/source/user_guide/advanced.rst | 14 +++++--------- doc/source/whatsnew/v1.0.0.rst | 4 ++-- pandas/core/indexes/base.py | 11 +++++++++++ pandas/core/indexes/category.py | 1 + pandas/core/indexes/datetimes.py | 1 + pandas/core/indexes/interval.py | 1 + pandas/core/indexes/multi.py | 3 +++ pandas/core/indexes/period.py | 1 + pandas/core/indexes/range.py | 1 + pandas/core/indexes/timedeltas.py | 1 + pandas/tests/indexes/multi/test_names.py | 9 +++++++++ 11 files changed, 36 insertions(+), 11 deletions(-) diff --git a/doc/source/user_guide/advanced.rst b/doc/source/user_guide/advanced.rst index 8223b831ebe2d..d6f5c0c758b60 100644 --- a/doc/source/user_guide/advanced.rst +++ b/doc/source/user_guide/advanced.rst @@ -565,19 +565,15 @@ When working with an ``Index`` object directly, rather than via a ``DataFrame``, mi2 = mi.rename("new name", level=0) mi2 -.. warning:: - Prior to pandas 1.0.0, you could also set the names of a ``MultiIndex`` - by updating the name of a level. +You cannot set the names of the MultiIndex via a level. - .. code-block:: none +.. ipython:: python + :okexcept: - >>> mi.levels[0].name = 'name via level' - >>> mi.names[0] # only works for older pandas - 'name via level' + mi.levels[0].name = "name via level" - As of pandas 1.0, this will *silently* fail to update the names - of the MultiIndex. Use :meth:`Index.set_names` instead. +Use :meth:`Index.set_names` instead. Sorting a ``MultiIndex`` ------------------------ diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 8755abe642068..0a29a168228d0 100755 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -255,10 +255,10 @@ For backwards compatibility, you can still *access* the names via the levels. mi.levels[0].name However, it is no longer possible to *update* the names of the ``MultiIndex`` -via the name of the level. The following will **silently** fail to update the -name of the ``MultiIndex`` +via the level. .. ipython:: python + :okexcept: mi.levels[0].name = "new name" mi.names diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index a3808f6f4a37e..b8659c2be8d44 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -240,6 +240,10 @@ def _outer_indexer(self, left, right): _data: Union[ExtensionArray, np.ndarray] _id = None _name: Optional[Hashable] = None + # MultiIndex.levels previously allowed setting the index name. We + # don't allow this anymore, and raise if it happens rather than + # failing silently. + _no_setting_name: bool = False _comparables = ["name"] _attributes = ["name"] _is_numeric_dtype = False @@ -520,6 +524,7 @@ def _simple_new(cls, values, name=None, dtype=None): # we actually set this value too. result._index_data = values result._name = name + result._no_setting_name = False return result._reset_identity() @@ -1214,6 +1219,12 @@ def name(self): @name.setter def name(self, value): + if self._no_setting_name: + # Used in MultiIndex.levels. + raise RuntimeError( + "Cannot set name on a level of a MultiIndex. Use " + "'MultiIndex.set_names' instead." + ) maybe_extract_name(value, None, type(self)) self._name = value diff --git a/pandas/core/indexes/category.py b/pandas/core/indexes/category.py index ba476f9e25ee6..579860f8557e7 100644 --- a/pandas/core/indexes/category.py +++ b/pandas/core/indexes/category.py @@ -253,6 +253,7 @@ def _simple_new(cls, values, name=None, dtype=None, **kwargs): setattr(result, k, v) result._reset_identity() + result._no_setting_name = False return result # -------------------------------------------------------------------- diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 108e24ffee820..a26bc193c3ee2 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -302,6 +302,7 @@ def _simple_new(cls, values, name=None, freq=None, tz=None, dtype=None): result = object.__new__(cls) result._data = dtarr result.name = name + result._no_setting_name = False # For groupby perf. See note in indexes/base about _index_data result._index_data = dtarr._data result._reset_identity() diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 52df491725504..6d12f56151ba9 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -234,6 +234,7 @@ def _simple_new(cls, array, name, closed=None): result = IntervalMixin.__new__(cls) result._data = array result.name = name + result._no_setting_name = False result._reset_identity() return result diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index dac9b20104c36..360cd3fdbaa3f 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -628,6 +628,9 @@ def levels(self): result = [ x._shallow_copy(name=name) for x, name in zip(self._levels, self._names) ] + for level in result: + # disallow midx.levels[0].name = "foo" + level._no_setting_name = True return FrozenList(result) @property diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 6465a0c1724af..0d50da7ad1e01 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -252,6 +252,7 @@ def _simple_new(cls, values, name=None, freq=None, **kwargs): # For groupby perf. See note in indexes/base about _index_data result._index_data = values._data result.name = name + result._no_setting_name = False result._reset_identity() return result diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index b4cc71a25792f..7fb6e1c60603d 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -141,6 +141,7 @@ def _simple_new(cls, values, name=None, dtype=None): result._range = values result.name = name + result._no_setting_name = False result._reset_identity() return result diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 480a4ae34bfb7..0c72a0f93f7e1 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -217,6 +217,7 @@ def _simple_new(cls, values, name=None, freq=None, dtype=_TD_DTYPE): result = object.__new__(cls) result._data = tdarr result._name = name + result._no_setting_name = False # For groupby perf. See note in indexes/base about _index_data result._index_data = tdarr._data diff --git a/pandas/tests/indexes/multi/test_names.py b/pandas/tests/indexes/multi/test_names.py index 5c3a48c9dd481..e2b07b503ccc6 100644 --- a/pandas/tests/indexes/multi/test_names.py +++ b/pandas/tests/indexes/multi/test_names.py @@ -124,3 +124,12 @@ def test_get_names_from_levels(): assert idx.levels[0].name == "a" assert idx.levels[1].name == "b" + + +def test_setting_names_from_levels_raises(): + idx = pd.MultiIndex.from_product([["a"], [1, 2]], names=["a", "b"]) + with pytest.raises(RuntimeError, match="set_names"): + idx.levels[0].name = "foo" + + with pytest.raises(RuntimeError, match="set_names"): + idx.levels[1].name = "foo"