Skip to content

ENH: Better rename/set_names handling for Index #4718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 1, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ See :ref:`Internal Refactoring<whatsnew_0130.refactoring>`
- Fix boolean comparison with a DataFrame on the lhs, and a list/tuple on the rhs (:issue:`4576`)
- Fix error/dtype conversion with setitem of ``None`` on ``Series/DataFrame`` (:issue:`4667`)
- Fix decoding based on a passed in non-default encoding in ``pd.read_stata`` (:issue:`4626`)
- Fix some inconsistencies with ``Index.rename`` and ``MultiIndex.rename``,
etc. (:issue:`4718`, :issue:`4628`)

pandas 0.12
===========
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def __hash__(self):
def _disabled(self, *args, **kwargs):
"""This method will not function because object is immutable."""
raise TypeError("'%s' does not support mutable operations." %
self.__class__)
self.__class__.__name__)

def __unicode__(self):
from pandas.core.common import pprint_thing
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,7 +1783,7 @@ def is_re_compilable(obj):


def is_list_like(arg):
return hasattr(arg, '__iter__') and not isinstance(arg, compat.string_types)
return hasattr(arg, '__iter__') and not isinstance(arg, compat.string_and_binary_types)


def _is_sequence(x):
Expand Down
22 changes: 20 additions & 2 deletions pandas/core/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,16 +273,33 @@ def set_names(self, names, inplace=False):

Returns
-------
new index (of same type and class...etc)
new index (of same type and class...etc) [if inplace, returns None]
"""
if not com.is_list_like(names):
raise TypeError("Must pass list-like as `names`.")
if inplace:
idx = self
else:
idx = self._shallow_copy()
idx._set_names(names)
return idx
if not inplace:
return idx

def rename(self, name, inplace=False):
"""
Set new names on index. Defaults to returning new index.

Parameters
----------
name : str or list
name to set
inplace : bool
if True, mutates in place

Returns
-------
new index (of same type and class...etc) [if inplace, returns None]
"""
return self.set_names([name], inplace=inplace)

@property
Expand Down Expand Up @@ -1556,6 +1573,7 @@ class MultiIndex(Index):
_levels = FrozenList()
_labels = FrozenList()
_comparables = ['names']
rename = Index.set_names

def __new__(cls, levels=None, labels=None, sortorder=None, names=None,
copy=False):
Expand Down
70 changes: 69 additions & 1 deletion pandas/tests/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pandas.compat import range, lrange, lzip, u, zip
import operator
import pickle
import re
import unittest
import nose
import os
Expand Down Expand Up @@ -44,10 +45,35 @@ def test_wrong_number_names(self):
def testit(ind):
ind.names = ["apple", "banana", "carrot"]

indices = (self.dateIndex, self.unicodeIndex, self.strIndex, self.intIndex, self.floatIndex, self.empty, self.tuples)
indices = (self.dateIndex, self.unicodeIndex, self.strIndex,
self.intIndex, self.floatIndex, self.empty, self.tuples)
for ind in indices:
assertRaisesRegexp(ValueError, "^Length", testit, ind)

def test_set_name_methods(self):
new_name = "This is the new name for this index"
indices = (self.dateIndex, self.intIndex, self.unicodeIndex,
self.empty)
for ind in indices:
original_name = ind.name
new_ind = ind.set_names([new_name])
self.assertEqual(new_ind.name, new_name)
self.assertEqual(ind.name, original_name)
res = ind.rename(new_name, inplace=True)
# should return None
self.assert_(res is None)
self.assertEqual(ind.name, new_name)
self.assertEqual(ind.names, [new_name])
with assertRaisesRegexp(TypeError, "list-like"):
# should still fail even if it would be the right length
ind.set_names("a")
# rename in place just leaves tuples and other containers alone
name = ('A', 'B')
ind = self.intIndex
ind.rename(name, inplace=True)
self.assertEqual(ind.name, name)
self.assertEqual(ind.names, [name])

def test_hash_error(self):
self.assertRaises(TypeError, hash, self.strIndex)

Expand Down Expand Up @@ -1018,6 +1044,48 @@ def setUp(self):
labels=[major_labels, minor_labels],
names=self.index_names)

def test_set_names_and_rename(self):
# so long as these are synonyms, we don't need to test set_names
self.assert_(self.index.rename == self.index.set_names)
new_names = [name + "SUFFIX" for name in self.index_names]
ind = self.index.set_names(new_names)
self.assertEqual(self.index.names, self.index_names)
self.assertEqual(ind.names, new_names)
with assertRaisesRegexp(ValueError, "^Length"):
ind.set_names(new_names + new_names)
new_names2 = [name + "SUFFIX2" for name in new_names]
res = ind.set_names(new_names2, inplace=True)
self.assert_(res is None)
self.assertEqual(ind.names, new_names2)

def test_set_levels_and_set_labels(self):
# side note - you probably wouldn't want to use levels and labels
# directly like this - but it is possible.
levels, labels = self.index.levels, self.index.labels
new_levels = [[lev + 'a' for lev in level] for level in levels]
major_labels, minor_labels = labels
major_labels = [(x + 1) % 3 for x in major_labels]
minor_labels = [(x + 1) % 1 for x in minor_labels]
new_labels = [major_labels, minor_labels]

def test_metadata_immutable(self):
levels, labels = self.index.levels, self.index.labels
# shouldn't be able to set at either the top level or base level
mutable_regex = re.compile('does not support mutable operations')
with assertRaisesRegexp(TypeError, mutable_regex):
levels[0] = levels[0]
with assertRaisesRegexp(TypeError, mutable_regex):
levels[0][0] = levels[0][0]
# ditto for labels
with assertRaisesRegexp(TypeError, mutable_regex):
labels[0] = labels[0]
with assertRaisesRegexp(TypeError, mutable_regex):
labels[0][0] = labels[0][0]
# and for names
names = self.index.names
with assertRaisesRegexp(TypeError, mutable_regex):
names[0] = names[0]

def test_copy_in_constructor(self):
levels = np.array(["a", "b", "c"])
labels = np.array([1, 1, 2, 0, 0, 1, 1])
Expand Down