Repository: Azure/msrest-for-python

Issue:
datetime returned by Deserializer.deserialize_rfc() is not picklable
### Repro

```python
import pickle
from msrest.serialization import Deserializer

datetime_rfc = "Mon, 25 May 2020 11:00:00 GMT"
datetime1 = Deserializer.deserialize_rfc(datetime_rfc)
print("datetime1: %s" % datetime1)
pickled = pickle.dumps(datetime1)
datetime2 = pickle.loads(pickled)
print("datetime2: %s" % datetime2)
```

### Output (msrest 0.6.13)
```
datetime1: 2020-05-25 11:00:00+00:00
datetime2: 2020-05-25 11:00:00+00:00
```

### Output (msrest 0.6.14)
```
datetime1: 2020-05-25 11:00:00+00:00
Traceback (most recent call last):
  File "d:\__temp\repro\main.py", line 8, in <module>
    datetime2 = pickle.loads(pickled)
TypeError: __init__() missing 1 required positional argument: 'offset'
```

### Details
This regression was introduced in https://github.com/Azure/msrest-for-python/pull/201.

After that change, in the example above `timedate1` is not picklable because `timedate1.tzinfo` contains an instance of [`_FixedOffset`](https://github.com/Azure/msrest-for-python/blob/v0.6.14/msrest/serialization.py#L82,L101) which is not picklable itself. `pickle.dumps(datetime1)` invokes `timedate1.tzinfo.__reduce__()`. `_FixedOffset` class doesn't define the [`__reduce__()`](https://docs.python.org/3/library/pickle.html#object.__reduce__) method and the implementation from its parent class is used. [`tzinfo.__reduce__()`](https://github.com/python/cpython/blob/v3.8.3/Lib/datetime.py#L1193,L1207) assumes that the class implements `__getinitargs__()` method. This is true for `datetime.timezone`, but not for `_FixedOffset`. Eventually, `pickle.loads(pickled)` tries to call `_FixedOffset.__init__()` without the required `offset` argument, resulting in a `TypeError`.

In practice the issue happens when trying to pickle/unpickle any object containing a `datetime` generated by `Deserializer.deserialize_rfc()`, e.g. with [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html).

### Potential solutions
1. Implement `_FixedOffset.__getinitargs__()`.
2. Implement `_FixedOffset.__reduce__()`.
2. Make `_FixedOffset` use the default implementation of `__reduce__()`, instead of one inherited from `datetime.tzinfo`:
`__reduce__ = object.__reduce__`

Once Python 2.7 compatibility is no longer required, `datetime.timezone` can be used instead of `_FixedOffset`.


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=h1) Report
> Merging [#208](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=desc) into [master](https://codecov.io/gh/Azure/msrest-for-python/commit/053469458820db6a33b2cb55b3f9a5e55a2f9716&el=desc) will **increase** coverage by `0.03%`.
> The diff coverage is `81.25%`.

[![Impacted file tree graph](https://codecov.io/gh/Azure/msrest-for-python/pull/208/graphs/tree.svg?width=650&height=150&src=pr&token=Lny6ZO1MUF)](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #208      +/-   ##
==========================================
+ Coverage   83.78%   83.81%   +0.03%     
==========================================
  Files          25       25              
  Lines        2664     2669       +5     
==========================================
+ Hits         2232     2237       +5     
  Misses        432      432              
```


| [Impacted Files](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [msrest/serialization.py](https://codecov.io/gh/Azure/msrest-for-python/pull/208/diff?src=pr&el=tree#diff-bXNyZXN0L3NlcmlhbGl6YXRpb24ucHk=) | `84.87% <81.25%> (+0.07%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=footer). Last update [0534694...31d717a](https://codecov.io/gh/Azure/msrest-for-python/pull/208?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/msrest/serialization.py b/msrest/serialization.py
index b3519f1..a8abd9a 100644
--- a/msrest/serialization.py
+++ b/msrest/serialization.py
@@ -79,26 +79,32 @@ class UTC(datetime.tzinfo):
         """No daylight saving for UTC."""
         return datetime.timedelta(hours=1)
 
-class _FixedOffset(datetime.tzinfo):
-    """Fixed offset in minutes east from UTC.
-    Copy/pasted from Python doc
-    :param int offset: offset in minutes
-    """
+try:
+    from datetime import timezone as _FixedOffset
+except ImportError:  # Python 2.7
+    class _FixedOffset(datetime.tzinfo):  # type: ignore
+        """Fixed offset in minutes east from UTC.
+        Copy/pasted from Python doc
+        :param datetime.timedelta offset: offset in timedelta format
+        """
 
-    def __init__(self, offset):
-        self.__offset = datetime.timedelta(minutes=offset)
+        def __init__(self, offset):
+            self.__offset = offset
 
-    def utcoffset(self, dt):
-        return self.__offset
+        def utcoffset(self, dt):
+            return self.__offset
 
-    def tzname(self, dt):
-        return str(self.__offset.total_seconds()/3600)
+        def tzname(self, dt):
+            return str(self.__offset.total_seconds()/3600)
 
-    def __repr__(self):
-        return "<FixedOffset {}>".format(self.tzname(None))
+        def __repr__(self):
+            return "<FixedOffset {}>".format(self.tzname(None))
 
-    def dst(self, dt):
-        return datetime.timedelta(0)
+        def dst(self, dt):
+            return datetime.timedelta(0)
+
+        def __getinitargs__(self):
+            return (self.__offset,)
 
 try:
     from datetime import timezone
@@ -1868,7 +1874,7 @@ class Deserializer(object):
             parsed_date = email.utils.parsedate_tz(attr)
             date_obj = datetime.datetime(
                 *parsed_date[:6],
-                tzinfo=_FixedOffset((parsed_date[9] or 0)/60)
+                tzinfo=_FixedOffset(datetime.timedelta(minutes=(parsed_date[9] or 0)/60))
             )
             if not date_obj.tzinfo:
                 date_obj = date_obj.astimezone(tz=TZ_UTC)


Test patch:
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index 5991753..cb41372 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -29,6 +29,7 @@ import sys
 import json
 import isodate
 import logging
+import pickle
 from enum import Enum
 from datetime import datetime, timedelta, date, time
 import unittest
@@ -2058,6 +2059,20 @@ class TestRuntimeDeserialized(unittest.TestCase):
         self.assertEqual(utc.tm_sec, 8)
         self.assertEqual(a.microsecond, 0)
 
+    def test_rfc_pickable(self):
+        """Check datetime created by RFC parser are pickable.
+
+        See https://github.com/Azure/msrest-for-python/issues/205
+        """
+
+        datetime_rfc = "Mon, 25 May 2020 11:00:00 GMT"
+        datetime1 = Deserializer.deserialize_rfc(datetime_rfc)
+
+        pickled = pickle.dumps(datetime1)
+        datetime2 = pickle.loads(pickled)
+
+        assert datetime1 == datetime2
+
     def test_polymorphic_deserialization(self):
 
         class Zoo(Model):