Repository: pandas-dev/pandas

Issue:
CLN: CacheableOffset not Properly Used
I believe that many of the `DateOffset`s that extend `CacheableOffset` do so incorrectly.

Looking at  `pandas.tseries.offsets`, there are some subclasses of `DateOffset` are declared as `class Foo(DateOffset, CacheableOffset)` and some as `class Foo(CacheableOffset, DateOffset)`.

All that `CacheableOffset` does is to set the class field `_cacheable = True`. `DateOffset` sets `_cacheable = False`. Therefore, depending on the order of the parent classes, in some cases `_cacheable` will be `False` and some cases `True`



Discussion:
ok...can you find an instance where this is 'wrong'? and a PR to clean up? (could just be some old code)

That code does not look old. I am not exactly sure how `_cacheable` is being used; I will have to look more closely. I can certainly provide a python example of how it is broken:

```
class Cache(object):
   _foo = True

class DT(object):
   _foo = False

class A(Cache, DT):
   pass

class Bob(DT, Cache):
   pass

B()._foo
False

A()._foo
True
```

On closer inspection, I do not see where either `_cacheable` or `_should_cache` is used.

I take that back. I found the following in `tseries.index`:

```
            if (offset._should_cache() and
                not (offset._normalize_cache and not _normalized) and
                    _naive_in_cache_range(start, end)):
                index = cls._cached_range(start, end, periods=periods,
                                          offset=offset, name=name)
            else:
                index = _generate_regular_range(start, end, periods, offset)
```


Resolving patch:
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- a/pandas/tseries/index.py
+++ b/pandas/tseries/index.py
@@ -341,9 +341,7 @@ def _generate(cls, start, end, periods, name, offset,
                 if end.tz is None and start.tz is not None:
                     end = end.tz_localize(start.tz)
 
-            if (offset._should_cache() and
-                not (offset._normalize_cache and not _normalized) and
-                    _naive_in_cache_range(start, end)):
+            if _use_cached_range(offset, _normalized, start, end):
                 index = cls._cached_range(start, end, periods=periods,
                                           offset=offset, name=name)
             else:
@@ -366,9 +364,7 @@ def _generate(cls, start, end, periods, name, offset,
                 if end.tz is None and start.tz is not None:
                     start = start.replace(tzinfo=None)
 
-            if (offset._should_cache() and
-                not (offset._normalize_cache and not _normalized) and
-                    _naive_in_cache_range(start, end)):
+            if _use_cached_range(offset, _normalized, start, end):
                 index = cls._cached_range(start, end, periods=periods,
                                           offset=offset, name=name)
             else:
@@ -1835,6 +1831,10 @@ def _naive_in_cache_range(start, end):
 def _in_range(start, end, rng_start, rng_end):
     return start > rng_start and end < rng_end
 
+def _use_cached_range(offset, _normalized, start, end):
+    return (offset._should_cache() and
+                not (offset._normalize_cache and not _normalized) and
+                    _naive_in_cache_range(start, end))
 
 def _time_to_micros(time):
     seconds = time.hour * 60 * 60 + 60 * time.minute + time.second
diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py
--- a/pandas/tseries/offsets.py
+++ b/pandas/tseries/offsets.py
@@ -477,7 +477,7 @@ def onOffset(self, dt):
         return np.is_busday(day64, busdaycal=self.busdaycalendar)
 
 
-class MonthEnd(DateOffset, CacheableOffset):
+class MonthEnd(CacheableOffset, DateOffset):
     """DateOffset of one month end"""
 
     def apply(self, other):
@@ -502,7 +502,7 @@ def rule_code(self):
         return 'M'
 
 
-class MonthBegin(DateOffset, CacheableOffset):
+class MonthBegin(CacheableOffset, DateOffset):
     """DateOffset of one month at beginning"""
 
     def apply(self, other):
@@ -553,7 +553,7 @@ def rule_code(self):
         return 'BM'
 
 
-class BusinessMonthBegin(DateOffset, CacheableOffset):
+class BusinessMonthBegin(CacheableOffset, DateOffset):
     """DateOffset of one business month at beginning"""
 
     def apply(self, other):
@@ -590,7 +590,7 @@ def rule_code(self):
         return 'BMS'
 
 
-class Week(DateOffset, CacheableOffset):
+class Week(CacheableOffset, DateOffset):
     """
     Weekly offset
 
@@ -656,7 +656,7 @@ def rule_code(self):
 }
 
 
-class WeekOfMonth(DateOffset, CacheableOffset):
+class WeekOfMonth(CacheableOffset, DateOffset):
     """
     Describes monthly dates like "the Tuesday of the 2nd week of each month"
 
@@ -729,7 +729,7 @@ def rule_code(self):
         return 'WOM' + suffix
 
 
-class BQuarterEnd(DateOffset, CacheableOffset):
+class BQuarterEnd(CacheableOffset, DateOffset):
     """DateOffset increments between business Quarter dates
     startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
     startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
@@ -796,7 +796,7 @@ def rule_code(self):
 }
 
 
-class BQuarterBegin(DateOffset, CacheableOffset):
+class BQuarterBegin(CacheableOffset, DateOffset):
     _outputName = "BusinessQuarterBegin"
 
     def __init__(self, n=1, **kwds):
@@ -843,7 +843,7 @@ def rule_code(self):
         return 'BQS' + suffix
 
 
-class QuarterEnd(DateOffset, CacheableOffset):
+class QuarterEnd(CacheableOffset, DateOffset):
     """DateOffset increments between business Quarter dates
     startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
     startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
@@ -887,7 +887,7 @@ def rule_code(self):
         return 'Q' + suffix
 
 
-class QuarterBegin(DateOffset, CacheableOffset):
+class QuarterBegin(CacheableOffset, DateOffset):
     _outputName = 'QuarterBegin'
 
     def __init__(self, n=1, **kwds):
@@ -924,7 +924,7 @@ def rule_code(self):
         return 'QS' + suffix
 
 
-class BYearEnd(DateOffset, CacheableOffset):
+class BYearEnd(CacheableOffset, DateOffset):
     """DateOffset increments between business EOM dates"""
     _outputName = 'BusinessYearEnd'
 
@@ -971,7 +971,7 @@ def rule_code(self):
         return 'BA' + suffix
 
 
-class BYearBegin(DateOffset, CacheableOffset):
+class BYearBegin(CacheableOffset, DateOffset):
     """DateOffset increments between business year begin dates"""
     _outputName = 'BusinessYearBegin'
 
@@ -1013,7 +1013,7 @@ def rule_code(self):
         return 'BAS' + suffix
 
 
-class YearEnd(DateOffset, CacheableOffset):
+class YearEnd(CacheableOffset, DateOffset):
     """DateOffset increments between calendar year ends"""
 
     def __init__(self, n=1, **kwds):
@@ -1080,7 +1080,7 @@ def rule_code(self):
         return 'A' + suffix
 
 
-class YearBegin(DateOffset, CacheableOffset):
+class YearBegin(CacheableOffset, DateOffset):
     """DateOffset increments between calendar year begin dates"""
 
     def __init__(self, n=1, **kwds):
@@ -1251,7 +1251,7 @@ def _delta_to_nanoseconds(delta):
             + delta.microseconds) * 1000
 
 
-class Day(Tick, CacheableOffset):
+class Day(CacheableOffset, Tick):
     _inc = timedelta(1)
     _rule_base = 'D'
 


Test patch:
diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py
--- a/pandas/tseries/tests/test_offsets.py
+++ b/pandas/tseries/tests/test_offsets.py
@@ -17,7 +17,7 @@
     get_standard_freq)
 
 from pandas.tseries.frequencies import _offset_map
-from pandas.tseries.index import _to_m8
+from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache
 from pandas.tseries.tools import parse_time_string
 import pandas.tseries.offsets as offsets
 
@@ -25,6 +25,7 @@
 from pandas.lib import Timestamp
 from pandas.util.testing import assertRaisesRegexp
 import pandas.util.testing as tm
+from pandas.tseries.offsets import BusinessMonthEnd, CacheableOffset
 
 _multiprocess_can_split_ = True
 
@@ -1789,7 +1790,60 @@ def test_freq_offsets():
 
     off = BDay(1, offset=timedelta(0, -1800))
     assert(off.freqstr == 'B-30Min')
-
+    
+def get_all_subclasses(cls):
+    ret = set()
+    this_subclasses = cls.__subclasses__()
+    ret = ret | set(this_subclasses)
+    for this_subclass in this_subclasses:
+        ret | get_all_subclasses(this_subclass)
+    return ret
+
+class TestCaching(unittest.TestCase):    
+    def test_should_cache_month_end(self):
+        self.assertTrue(MonthEnd()._should_cache())
+        
+    def test_should_cache_bmonth_end(self):
+        self.assertTrue(BusinessMonthEnd()._should_cache())
+        
+    def test_should_cache_week_month(self):
+        self.assertTrue(WeekOfMonth(weekday=1, week=2)._should_cache())
+        
+    def test_all_cacheableoffsets(self):
+        for subclass in get_all_subclasses(CacheableOffset):
+            if subclass in [WeekOfMonth]:
+                continue
+            self.run_X_index_creation(subclass)
+            
+    def setUp(self):
+        _daterange_cache.clear()
+        
+    def run_X_index_creation(self, cls):
+        inst1 = cls()
+        if not inst1.isAnchored():
+            self.assertFalse(inst1._should_cache(), cls)
+            return
+        
+        self.assertTrue(inst1._should_cache(), cls)
+            
+        DatetimeIndex(start=datetime(2013,1,31), end=datetime(2013,3,31), freq=inst1, normalize=True)
+        self.assertTrue(cls() in _daterange_cache, cls)
+                
+    def test_month_end_index_creation(self):
+        DatetimeIndex(start=datetime(2013,1,31), end=datetime(2013,3,31), freq=MonthEnd(), normalize=True)
+        self.assertTrue(MonthEnd() in _daterange_cache)
+        
+    def test_bmonth_end_index_creation(self):
+        DatetimeIndex(start=datetime(2013,1,31), end=datetime(2013,3,29), freq=BusinessMonthEnd(), normalize=True)
+        self.assertTrue(BusinessMonthEnd() in _daterange_cache)
+
+    def test_week_of_month_index_creation(self):
+        inst1 = WeekOfMonth(weekday=1, week=2)
+        DatetimeIndex(start=datetime(2013,1,31), end=datetime(2013,3,29), freq=inst1, normalize=True)
+        inst2 = WeekOfMonth(weekday=1, week=2)
+        self.assertTrue(inst2 in _daterange_cache)
+        
+         
 if __name__ == '__main__':
     import nose
     nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],