Repository: pandas-dev/pandas

Issue:
BUG: pd.frequency.get_offset cache may be overwritten
Because `frequencies.get_offset` may be broken by user operation because of its cache.

https://github.com/pydata/pandas/blob/master/pandas/tseries/frequencies.py#L515

```
import pandas as pd
x = pd.tseries.frequencies.get_offset('D')
x.__dict__
# {'normalize': False, 'kwds': {}, 'n': 1, '_offset': datetime.timedelta(1), '_named': 'D', '_use_relativedelta': False}

# modify its property
x.n = 5

# NG, modified instance is returned
y = pd.tseries.frequencies.get_offset('D')
y.__dict__
# {'normalize': False, 'kwds': {}, 'n': 5, '_offset': datetime.timedelta(1), '_named': 'D', '_use_relativedelta': False}
```

It should return a copy of the cache.



Discussion:
yeh these are class properties, but `get_offset` should prob return a copy. Though need to check for perf impacts on this.

[`n` is an instance property](https://github.com/pydata/pandas/blob/master/pandas/tseries/offsets.py#L182) specify its length/span. I don't think user touch it actually though... 

From what I've seen, there is no function uses freq taken from `get_offset` as it is. Applying numeric ops (like multiplying freq mult) will results in copy. Thus, user ops should not affect to internal cache.

```
pd.tseries.frequencies.get_offset('A')
# A-DEC
pd.tseries.frequencies.get_offset('A') * 1
# <YearEnd: month=12>
```

Above repr difference is caused by `_named` property, which looks to be attached by [`_make_offset`](https://github.com/pydata/pandas/blob/master/pandas/tseries/offsets.py#L2669). It is introduced in #5189 for repr backcompat. I don't think it is needed anymore (because user can't get freq from `get_offset`).

Maybe we can cleanup freq-retrieving functions in `freqnencies.py` and guarantee user can't touch the internal cache.


Resolving patch:
diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py
--- a/pandas/tseries/frequencies.py
+++ b/pandas/tseries/frequencies.py
@@ -286,8 +286,7 @@ def _get_freq_str(base, mult=1):
                                     MonthEnd, BMonthBegin, BMonthEnd,
                                     QuarterBegin, QuarterEnd, BQuarterBegin,
                                     BQuarterEnd, YearBegin, YearEnd,
-                                    BYearBegin, BYearEnd, _make_offset
-                                    )
+                                    BYearBegin, BYearEnd, prefix_mapping)
 try:
     cday = CDay()
 except NotImplementedError:
@@ -547,13 +546,17 @@ def get_offset(name):
 
     if name not in _offset_map:
         try:
-            # generate and cache offset
-            offset = _make_offset(name)
+            split = name.split('-')
+            klass = prefix_mapping[split[0]]
+            # handles case where there's no suffix (and will TypeError if too many '-')
+            offset = klass._from_name(*split[1:])
         except (ValueError, TypeError, KeyError):
             # bad prefix or suffix
             raise ValueError('Bad rule name requested: %s.' % name)
+        # cache
         _offset_map[name] = offset
-    return _offset_map[name]
+    # do not return cache because it's mutable
+    return _offset_map[name].copy()
 
 
 getOffset = get_offset
@@ -567,19 +570,10 @@ def get_offset_name(offset):
     --------
     get_offset_name(BMonthEnd(1)) --> 'EOM'
     """
-    if offset is None:
-        raise ValueError("Offset can't be none!")
-    # Hack because this is what it did before...
-    if isinstance(offset, BDay):
-        if offset.n != 1:
-            raise ValueError('Bad rule given: %s.' % 'BusinessDays')
-        else:
-            return offset.rule_code
-    try:
-        return offset.freqstr
-    except AttributeError:
-        # Bad offset, give useful error.
-        raise ValueError('Bad rule given: %s.' % offset)
+
+    msg = "get_offset_name(offset) is deprecated. Use offset.freqstr instead"
+    warnings.warn(msg, FutureWarning, stacklevel=2)
+    return offset.freqstr
 
 
 def get_legacy_offset_name(offset):
diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py
--- a/pandas/tseries/offsets.py
+++ b/pandas/tseries/offsets.py
@@ -309,8 +309,6 @@ def _params(self):
         return params
 
     def __repr__(self):
-        if hasattr(self, '_named'):
-            return self._named
         className = getattr(self, '_outputName', type(self).__name__)
         exclude = set(['n', 'inc', 'normalize'])
         attrs = []
@@ -346,10 +344,7 @@ def __repr__(self):
 
     @property
     def name(self):
-        if hasattr(self, '_named'):
-            return self._named
-        else:
-            return self.rule_code
+        return self.rule_code
 
     def __eq__(self, other):
         if other is None:
@@ -516,8 +511,6 @@ class BusinessMixin(object):
     # attributes on each object rather than the existing behavior of iterating
     # over internal ``__dict__``
     def __repr__(self):
-        if hasattr(self, '_named'):
-            return self._named
         className = getattr(self, '_outputName', self.__class__.__name__)
 
         if abs(self.n) != 1:
@@ -2668,16 +2661,3 @@ def generate_range(start=None, end=None, periods=None,
 ])
 
 prefix_mapping['N'] = Nano
-
-def _make_offset(key):
-    """Gets offset based on key. KeyError if prefix is bad, ValueError if
-    suffix is bad. All handled by `get_offset` in tseries/frequencies. Not
-    public."""
-    if key is None:
-        return None
-    split = key.split('-')
-    klass = prefix_mapping[split[0]]
-    # handles case where there's no suffix (and will TypeError if too many '-')
-    obj = klass._from_name(*split[1:])
-    obj._named = key
-    return obj


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
@@ -16,7 +16,7 @@
     QuarterBegin, BQuarterBegin, BMonthBegin, DateOffset, Week,
     YearBegin, YearEnd, Hour, Minute, Second, Day, Micro, Milli, Nano, Easter,
     WeekOfMonth, format, ole2datetime, QuarterEnd, to_datetime, normalize_date,
-    get_offset, get_offset_name, get_standard_freq)
+    get_offset, get_standard_freq)
 
 from pandas import Series
 from pandas.tseries.frequencies import _offset_map, get_freq_code, _get_freq_str
@@ -3593,19 +3593,20 @@ def test_compare_ticks(self):
 
 class TestOffsetNames(tm.TestCase):
     def test_get_offset_name(self):
-        assertRaisesRegexp(ValueError, 'Bad rule.*BusinessDays', get_offset_name, BDay(2))
-
-        assert get_offset_name(BDay()) == 'B'
-        assert get_offset_name(BMonthEnd()) == 'BM'
-        assert get_offset_name(Week(weekday=0)) == 'W-MON'
-        assert get_offset_name(Week(weekday=1)) == 'W-TUE'
-        assert get_offset_name(Week(weekday=2)) == 'W-WED'
-        assert get_offset_name(Week(weekday=3)) == 'W-THU'
-        assert get_offset_name(Week(weekday=4)) == 'W-FRI'
-
-        self.assertEqual(get_offset_name(LastWeekOfMonth(weekday=WeekDay.SUN)), "LWOM-SUN")
-        self.assertEqual(get_offset_name(makeFY5253LastOfMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=4)),"REQ-L-MAR-TUE-4")
-        self.assertEqual(get_offset_name(makeFY5253NearestEndMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=3)), "REQ-N-MAR-TUE-3")
+        self.assertEqual(BDay().freqstr, 'B')
+        self.assertEqual(BDay(2).freqstr, '2B')
+        self.assertEqual(BMonthEnd().freqstr, 'BM')
+        self.assertEqual(Week(weekday=0).freqstr, 'W-MON')
+        self.assertEqual(Week(weekday=1).freqstr, 'W-TUE')
+        self.assertEqual(Week(weekday=2).freqstr, 'W-WED')
+        self.assertEqual(Week(weekday=3).freqstr, 'W-THU')
+        self.assertEqual(Week(weekday=4).freqstr, 'W-FRI')
+
+        self.assertEqual(LastWeekOfMonth(weekday=WeekDay.SUN).freqstr, "LWOM-SUN")
+        self.assertEqual(makeFY5253LastOfMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=4).freqstr,
+                         "REQ-L-MAR-TUE-4")
+        self.assertEqual(makeFY5253NearestEndMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=3).freqstr,
+                         "REQ-N-MAR-TUE-3")
 
 def test_get_offset():
     assertRaisesRegexp(ValueError, "rule.*GIBBERISH", get_offset, 'gibberish')
@@ -3834,13 +3835,10 @@ def test_str_for_named_is_name(self):
         names += ['W-' + day for day in days]
         names += ['WOM-' + week + day for week in ('1', '2', '3', '4')
                                        for day in days]
-        #singletons
-        names += ['S', 'T', 'U', 'BM', 'BMS', 'BQ', 'QS'] # No 'Q'
         _offset_map.clear()
         for name in names:
             offset = get_offset(name)
-            self.assertEqual(repr(offset), name)
-            self.assertEqual(str(offset), name)
+            self.assertEqual(offset.freqstr, name)
 
 
 def get_utc_offset_hours(ts):