Repository: pandas-dev/pandas

Issue:
Add cache resetting method to PandasObject
Going to use this to fix MI, but also worth it anywhere we use cached_readonly and only hardcode cache name and implementation in a few places. (even on objects which supposedly immutable). 

BUG: MI needs to invalidate all of its cached properties when levels or labels are set.
Whack-a-mole!  @jreback is there some kind of standardized method for resetting the `_cache` attribute? I was thinking we could add a `_reset_cache` method to `PandasObject` that resets the attribute (so we only have to change it in one or two places).



Discussion:

cache only exists on NDFrame afaik

Lexsort_depth among others is cached_readonly
On Oct 14, 2013 7:05 PM, "jreback" notifications@github.com wrote:

> cache only exists on NDFrame afaik
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/pydata/pandas/issues/5225#issuecomment-26296260
> .


Resolving patch:
diff --git a/pandas/core/base.py b/pandas/core/base.py
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -48,16 +48,6 @@ def __repr__(self):
         """
         return str(self)
 
-    def _local_dir(self):
-        """ provide addtional __dir__ for this object """
-        return []
-
-    def __dir__(self):
-        """
-        Provide method name lookup and completion
-        Only provide 'public' methods
-        """
-        return list(sorted(list(set(dir(type(self)) + self._local_dir()))))
 
 class PandasObject(StringMixin):
     """baseclass for various pandas objects"""
@@ -77,6 +67,29 @@ def __unicode__(self):
         # Should be overwritten by base classes
         return object.__repr__(self)
 
+    def _local_dir(self):
+        """ provide addtional __dir__ for this object """
+        return []
+
+    def __dir__(self):
+        """
+        Provide method name lookup and completion
+        Only provide 'public' methods
+        """
+        return list(sorted(list(set(dir(type(self)) + self._local_dir()))))
+
+    def _reset_cache(self, key=None):
+        """
+        Reset cached properties. If ``key`` is passed, only clears that key.
+        """
+        if getattr(self, '_cache', None) is None:
+            return
+        if key is None:
+            self._cache.clear()
+        else:
+            self._cache.pop(key, None)
+
+
 class FrozenList(PandasObject, list):
     """
     Container that doesn't allow setting item *but*
diff --git a/pandas/core/index.py b/pandas/core/index.py
--- a/pandas/core/index.py
+++ b/pandas/core/index.py
@@ -1929,7 +1929,9 @@ def _set_levels(self, levels, copy=False, validate=True,
         self._levels = levels
         if any(names):
             self._set_names(names)
+
         self._tuples = None
+        self._reset_cache()
 
         if verify_integrity:
             self._verify_integrity()
@@ -1981,6 +1983,7 @@ def _set_labels(self, labels, copy=False, validate=True,
         self._labels = FrozenList(_ensure_frozen(labs, copy=copy)._shallow_copy()
                                   for labs in labels)
         self._tuples = None
+        self._reset_cache()
 
         if verify_integrity:
             self._verify_integrity()


Test patch:
diff --git a/pandas/tests/test_index.py b/pandas/tests/test_index.py
--- a/pandas/tests/test_index.py
+++ b/pandas/tests/test_index.py
@@ -2462,6 +2462,16 @@ def test_isnull_behavior(self):
         with tm.assertRaises(NotImplementedError):
             pd.isnull(self.index)
 
+    def test_level_setting_resets_attributes(self):
+        ind = MultiIndex.from_arrays([
+            ['A', 'A', 'B', 'B', 'B'],
+            [1, 2, 1, 2, 3]])
+        assert ind.is_monotonic
+        ind.set_levels([['A', 'B', 'A', 'A', 'B'], [2, 1, 3, -2, 5]],
+                       inplace=True)
+        # if this fails, probably didn't reset the cache correctly.
+        assert not ind.is_monotonic
+
 
 def test_get_combined_index():
     from pandas.core.index import _get_combined_index