diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index 454ffc5e5c685..50b4b1176c6cb 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -1578,3 +1578,4 @@ Bug Fixes - Bugs in ``stack``, ``get_dummies``, ``make_axis_dummies`` which don't preserve categorical dtypes in (multi)indexes (:issue:`13854`) - ``PeridIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`) - Bug in ``df.groupby`` where ``.median()`` returns arbitrary values if grouped dataframe contains empty bins (:issue:`13629`) +- Bug in ``Index.copy()`` where ``name`` parameter was ignored (:issue:`14302`) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index f430305f5cb91..0f68272117cc8 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -620,26 +620,40 @@ def _coerce_scalar_to_index(self, item): @Appender(_index_shared_docs['copy']) def copy(self, name=None, deep=False, dtype=None, **kwargs): - names = kwargs.get('names') - if names is not None and name is not None: - raise TypeError("Can only provide one of `names` and `name`") if deep: - from copy import deepcopy new_index = self._shallow_copy(self._data.copy()) - name = name or deepcopy(self.name) else: new_index = self._shallow_copy() - name = self.name - if name is not None: - names = [name] - if names: - new_index = new_index.set_names(names) + + names = kwargs.get('names') + names = self._validate_names(name=name, names=names, deep=deep) + new_index = new_index.set_names(names) + if dtype: new_index = new_index.astype(dtype) return new_index __copy__ = copy + def _validate_names(self, name=None, names=None, deep=False): + """ + Handles the quirks of having a singular 'name' parameter for general + Index and plural 'names' parameter for MultiIndex. + """ + from copy import deepcopy + if names is not None and name is not None: + raise TypeError("Can only provide one of `names` and `name`") + elif names is None and name is None: + return deepcopy(self.names) if deep else self.names + elif names is not None: + if not is_list_like(names): + raise TypeError("Must pass list-like as `names`.") + return names + else: + if not is_list_like(name): + return [name] + return name + def __unicode__(self): """ Return a string representation for this object. diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index 09c755b2c9792..e6aefaeb01a15 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -346,7 +346,7 @@ def set_labels(self, labels, level=None, inplace=False, labels = property(fget=_get_labels, fset=__set_labels) def copy(self, names=None, dtype=None, levels=None, labels=None, - deep=False, _set_identity=False): + deep=False, _set_identity=False, **kwargs): """ Make a copy of this object. Names, dtype, levels and labels can be passed and will be set on new copy. @@ -368,15 +368,20 @@ def copy(self, names=None, dtype=None, levels=None, labels=None, ``deep``, but if ``deep`` is passed it will attempt to deepcopy. This could be potentially expensive on large MultiIndex objects. """ + name = kwargs.get('name') + names = self._validate_names(name=name, names=names, deep=deep) + if deep: from copy import deepcopy - levels = levels if levels is not None else deepcopy(self.levels) - labels = labels if labels is not None else deepcopy(self.labels) - names = names if names is not None else deepcopy(self.names) + if levels is None: + levels = deepcopy(self.levels) + if labels is None: + labels = deepcopy(self.labels) else: - levels = self.levels - labels = self.labels - names = self.names + if levels is None: + levels = self.levels + if labels is None: + labels = self.labels return MultiIndex(levels=levels, labels=labels, names=names, sortorder=self.sortorder, verify_integrity=False, _set_identity=_set_identity) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 7f68318d4d7d3..61b70c738aa95 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1817,6 +1817,30 @@ def test_copy_name(self): s3 = s1 * s2 self.assertEqual(s3.index.name, 'mario') + def test_copy_name2(self): + # Check that adding a "name" parameter to the copy is honored + # GH14302 + idx = pd.Index([1, 2], name='MyName') + idx1 = idx.copy() + + self.assertTrue(idx.equals(idx1)) + self.assertEqual(idx.name, 'MyName') + self.assertEqual(idx1.name, 'MyName') + + idx2 = idx.copy(name='NewName') + + self.assertTrue(idx.equals(idx2)) + self.assertEqual(idx.name, 'MyName') + self.assertEqual(idx2.name, 'NewName') + + idx3 = idx.copy(names=['NewName']) + + self.assertTrue(idx.equals(idx3)) + self.assertEqual(idx.name, 'MyName') + self.assertEqual(idx.names, ['MyName']) + self.assertEqual(idx3.name, 'NewName') + self.assertEqual(idx3.names, ['NewName']) + def test_union_base(self): idx = self.create_index() first = idx[3:] diff --git a/pandas/tests/indexes/test_multi.py b/pandas/tests/indexes/test_multi.py index 5248f0775d22f..46fd811e5f14c 100644 --- a/pandas/tests/indexes/test_multi.py +++ b/pandas/tests/indexes/test_multi.py @@ -415,6 +415,28 @@ def test_set_value_keeps_names(self): self.assertIsNone(df.is_copy) self.assertEqual(df.index.names, ('Name', 'Number')) + def test_copy_names(self): + # Check that adding a "names" parameter to the copy is honored + # GH14302 + multi_idx = pd.Index([(1, 2), (3, 4)], names=['MyName1', 'MyName2']) + multi_idx1 = multi_idx.copy() + + self.assertTrue(multi_idx.equals(multi_idx1)) + self.assertEqual(multi_idx.names, ['MyName1', 'MyName2']) + self.assertEqual(multi_idx1.names, ['MyName1', 'MyName2']) + + multi_idx2 = multi_idx.copy(names=['NewName1', 'NewName2']) + + self.assertTrue(multi_idx.equals(multi_idx2)) + self.assertEqual(multi_idx.names, ['MyName1', 'MyName2']) + self.assertEqual(multi_idx2.names, ['NewName1', 'NewName2']) + + multi_idx3 = multi_idx.copy(name=['NewName1', 'NewName2']) + + self.assertTrue(multi_idx.equals(multi_idx3)) + self.assertEqual(multi_idx.names, ['MyName1', 'MyName2']) + self.assertEqual(multi_idx3.names, ['NewName1', 'NewName2']) + def test_names(self): # names are assigned in __init__