Repository: dr-prodigy/python-holidays

Issue:
Can't un-pickle a `HolidayBase`
Seems that after a holidays class, e.g. `holidays.UnitedStates()` is used once, it can't be un-pickled.

For example, this snippet:

```python
import holidays
import pickle
from datetime import datetime

# Works:
us_holidays = holidays.UnitedStates()
us_holidays_ = pickle.loads(pickle.dumps(us_holidays))
b = datetime.fromisoformat("2020-01-01") in us_holidays_

# Fails:
us_holidays = holidays.UnitedStates()
b = datetime.fromisoformat("2020-01-01") in us_holidays
dump = pickle.dumps(us_holidays)
pickle.loads(dump)  # <- exception
```

Raises the following exception from the last line:
```
~/.local/share/virtualenvs/sibylla-v2-LxBhzJgn/lib/python3.8/site-packages/holidays/holiday_base.py in __setitem__(self, key, value)
    116
    117     def __setitem__(self, key, value):
--> 118         if key in self:
    119             if self.get(key).find(value) < 0 \
    120                     and value.find(self.get(key)) < 0:

~/.local/share/virtualenvs/sibylla-v2-LxBhzJgn/lib/python3.8/site-packages/holidays/holiday_base.py in __contains__(self, key)
     73
     74     def __contains__(self, key):
---> 75         return dict.__contains__(self, self.__keytransform__(key))
     76
     77     def __getitem__(self, key):

~/.local/share/virtualenvs/sibylla-v2-LxBhzJgn/lib/python3.8/site-packages/holidays/holiday_base.py in __keytransform__(self, key)
     67             raise TypeError("Cannot convert type '%s' to date." % type(key))
     68
---> 69         if self.expand and key.year not in self.years:
     70             self.years.add(key.year)
     71             self._populate(key.year)
```

The `expand` attribute is set by `__init__`, but it's not there during deserialization via unpickling.
I think it's because the `HolidayBase` inherits from dict and there's some weirdness there - it seems to first populate the dict in the deserialized object and only then sets the attributes from the state. But since `HolidayBase` overrides `__setitem__` and in this override it's using state attributes that weren't yet set on the object, the `expand` attribute is missing.

Tested with `holidays=='0.10.4'`.


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index f54702d9..4a4c5a9f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -15,6 +15,7 @@ Released -
 - Japan fix #445 (osoken)
 - Serbia fix #446 (kosugor)
 - United Kingdom get_list fix #448 (bletham)
+- Singapore fix for multi-year #419 (mborsetti)
 
 
 Version 0.10.5.2
diff --git a/holidays/countries/singapore.py b/holidays/countries/singapore.py
index 8993023e..30640382 100644
--- a/holidays/countries/singapore.py
+++ b/holidays/countries/singapore.py
@@ -268,7 +268,7 @@ class Singapore(HolidayBase):
                 in_lieu_date = hol_date + rd(days=+1)
                 while in_lieu_date in self:
                     in_lieu_date += rd(days=+1)
-                self[in_lieu_date] = hol_name + "[In lieu]"
+                self[in_lieu_date] = hol_name + " [In lieu]"
 
     # The below is used to calculate lunar new year (i.e. Chinese new year)
     # Code borrowed from Hong Kong entry as of 16-Nov-19
diff --git a/holidays/countries/south_africa.py b/holidays/countries/south_africa.py
index 93191e3a..02790692 100644
--- a/holidays/countries/south_africa.py
+++ b/holidays/countries/south_africa.py
@@ -100,7 +100,7 @@ class SouthAfrica(HolidayBase):
         # As of 1995/1/1, whenever a public holiday falls on a Sunday,
         # it rolls over to the following Monday
         for k, v in list(self.items()):
-            if self.observed and year > 1994 and k.weekday() == SUN:
+            if self.observed and year > 1994 and k.weekday() == SUN and k.year == year:
                 add_days = 1
                 while self.get(k + rd(days=add_days)) is not None:
                     add_days += 1
diff --git a/holidays/holiday_base.py b/holidays/holiday_base.py
index 1ca61fcc..a2441015 100644
--- a/holidays/holiday_base.py
+++ b/holidays/holiday_base.py
@@ -209,6 +209,9 @@ class HolidayBase(dict):
     def _populate(self, year):
         pass
 
+    def __reduce__(self):
+        return super(HolidayBase, self).__reduce__()
+
 
 def createHolidaySum(h1, h2):
     class HolidaySum(HolidayBase):


Test patch:
diff --git a/test/test_holiday_base.py b/test/test_holiday_base.py
index 9211bf3c..96261675 100644
--- a/test/test_holiday_base.py
+++ b/test/test_holiday_base.py
@@ -11,6 +11,7 @@
 #  Website: https://github.com/dr-prodigy/python-holidays
 #  License: MIT (see LICENSE file)
 
+import pickle
 import unittest
 
 from datetime import date, datetime, timedelta
@@ -447,6 +448,16 @@ class TestArgs(unittest.TestCase):
         self.holidays.observed = True
         self.assertIn(date(2018, 7, 2), self.holidays)
 
+    def test_serialization(self):
+        loaded_holidays = pickle.loads(pickle.dumps(self.holidays))
+        assert loaded_holidays == self.holidays
+
+        dt = datetime(2020, 1, 1)
+        res = dt in self.holidays
+        loaded_holidays = pickle.loads(pickle.dumps(self.holidays))
+        assert loaded_holidays == self.holidays
+        assert (dt in loaded_holidays) == res
+
 
 class TestKeyTransforms(unittest.TestCase):
     def setUp(self):