From c95fc721636ae8adc44d23446881b1a78431ef4e Mon Sep 17 00:00:00 2001 From: Jeffrey Tratner Date: Sat, 31 Aug 2013 17:14:45 -0400 Subject: [PATCH] ENH: Better rename/set_names handling for Index * MultiIndex now responds correctly to ``rename`` (synonym for ``set_names``) * ``set_names`` checks that input is list like. * Cleaner error message for immutable ops. * Added in previously missing tests for MultiIndex immutable metadata. * Altered ``is_list_like`` to check for binary_types as well. --- doc/source/release.rst | 2 ++ pandas/core/base.py | 2 +- pandas/core/common.py | 2 +- pandas/core/index.py | 22 ++++++++++-- pandas/tests/test_index.py | 70 +++++++++++++++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 5 deletions(-) diff --git a/doc/source/release.rst b/doc/source/release.rst index 570300b7c79de..6530b7e5e9238 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -306,6 +306,8 @@ See :ref:`Internal 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 =========== diff --git a/pandas/core/base.py b/pandas/core/base.py index 04f48f85fa023..a57af06f24cc9 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -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 diff --git a/pandas/core/common.py b/pandas/core/common.py index 0d3bd1a0c6de2..a995881d5c1e9 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -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): diff --git a/pandas/core/index.py b/pandas/core/index.py index 05eb53a444294..91e4d51c6c0ad 100644 --- a/pandas/core/index.py +++ b/pandas/core/index.py @@ -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 @@ -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): diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py index 16f3026896d4f..410d310e002b2 100644 --- a/pandas/tests/test_index.py +++ b/pandas/tests/test_index.py @@ -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 @@ -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) @@ -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])