Repository: zopefoundation/DateTime (language: python)

Issue:
`pytz.exceptions.UnknownTimeZoneError` when unpickling `DateTime.DateTime('GMT+1').asdatetime()`
## BUG/PROBLEM REPORT / FEATURE REQUEST

<!--

Please do not report security-related issues here. Report them by email to security@plone.org. The Security Team will contact the relevant maintainer if necessary.

Include tracebacks, screenshots, code of debugging sessions or code that reproduces the issue if possible.
The best reproductions are in plain Zope installations without addons or at least with minimal needed addons installed.

-->

### What I did:

`DateTime.asdatetime` converts to `datetime` carrying the same date, including timezone, but such datetimes cannot be pickled:

```
>>> import DateTime, pickle; pickle.loads(pickle.dumps(DateTime.DateTime('GMT+1').asdatetime()))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/srv/slapgrid/slappart30/srv/project/zope/venv/lib/python3.9/site-packages/pytz/__init__.py", line 307, in _p
    return unpickler(*args)
  File "/srv/slapgrid/slappart30/srv/project/zope/venv/lib/python3.9/site-packages/pytz/tzinfo.py", line 542, in unpickler
    tz = pytz.timezone(zone)
  File "/srv/slapgrid/slappart30/srv/project/zope/venv/lib/python3.9/site-packages/pytz/__init__.py", line 188, in timezone
    raise UnknownTimeZoneError(zone)
pytz.exceptions.UnknownTimeZoneError: 'GMT+1'
```

### What I expect to happen:

the datetime instance can be unpickled

### What actually happened:

an `UnknownTimeZoneError` is raised

### What version of Python and Zope/Addons I am using:

pytz 2023.3.post1
DateTime current master branch ( 310488279d2cefe53f8f5f77020b72fbf2ddac0c )


### More information:

DateTime constructs its own timezones with `GMT+1` names that are defined here:

https://github.com/zopefoundation/DateTime/blob/310488279d2cefe53f8f5f77020b72fbf2ddac0c/src/DateTime/pytz_support.py#L27-L30

and used to create a registry with instances of a subclass of `pytz.tzinfo.StaticTzInfo` here:

https://github.com/zopefoundation/DateTime/blob/310488279d2cefe53f8f5f77020b72fbf2ddac0c/src/DateTime/pytz_support.py#L202-L212

unpickling such an instance is causing the `UnknownTimeZoneError` exception, because `pytz.tzinfo.StaticTzInfo` defines [`__reduce__`](https://github.com/stub42/pytz/blob/release_2023.3/src/pytz/tzinfo.py#L150-L153) which calls [`_p`](https://github.com/stub42/pytz/blob/release_2023.3/src/pytz/__init__.py#L301-L307), which is just a wrapper calling [`unpickler`](https://github.com/stub42/pytz/blob/release_2023.3/src/pytz/tzinfo.py#L529-L577) which basically does this:

```py
def unpickler(zone):
    ...
    if zone not in _tzinfo_cache:
        if zone in all_timezones_set:  # noqa
            fp = open_resource(zone)
            try:
                _tzinfo_cache[zone] = build_tzinfo(zone, fp)
            finally:
                fp.close()
        else:
            raise UnknownTimeZoneError(zone)
```

where the `UnknownTimeZoneError` is raised.

I believe the fix can be to adjust `_static_timezone_factory` to create classes with a `__reduce__` understanding the extra timezones from `DateTime.pytz_support`, I'm preparing a patch.



Discussion / hints:

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c2c751e..3ee820c 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,7 +4,8 @@ Changelog
 5.4 (unreleased)
 ----------------
 
-- Nothing changed yet.
+- Fix ``UnknownTimeZoneError`` when unpickling ``DateTime.DateTime().asdatetime()``.
+  (`#58 <https://github.com/zopefoundation/DateTime/issues/58>`_)
 
 
 5.3 (2023-11-14)
diff --git a/src/DateTime/pytz_support.py b/src/DateTime/pytz_support.py
index 7eea255..4acf324 100644
--- a/src/DateTime/pytz_support.py
+++ b/src/DateTime/pytz_support.py
@@ -199,9 +199,14 @@
     _old_zmap['+%s00' % fhour] = 'GMT+%i' % hour
 
 
+def _p(zone):
+    return _numeric_timezones[zone]
+
+
 def _static_timezone_factory(data):
     zone = data[0]
     cls = type(zone, (StaticTzInfo,), dict(
+        __reduce__=lambda _: (_p, (zone, )),
         zone=zone,
         _utcoffset=memorized_timedelta(data[5][0][0]),
         _tzname=data[6][:-1]))  # strip the trailing null


Test patch:
diff --git a/src/DateTime/tests/test_datetime.py b/src/DateTime/tests/test_datetime.py
index 9dbd423..827d002 100644
--- a/src/DateTime/tests/test_datetime.py
+++ b/src/DateTime/tests/test_datetime.py
@@ -259,6 +259,13 @@ def test_pickle_with_tz(self):
         for key in DateTime.__slots__:
             self.assertEqual(getattr(dt, key), getattr(new, key))
 
+    def test_pickle_asdatetime_with_tz(self):
+        dt = DateTime('2002/5/2 8:00am GMT+8')
+        data = pickle.dumps(dt.asdatetime(), 1)
+        new = DateTime(pickle.loads(data))
+        for key in DateTime.__slots__:
+            self.assertEqual(getattr(dt, key), getattr(new, key))
+
     def test_pickle_with_numerical_tz(self):
         for dt_str in ('2007/01/02 12:34:56.789 +0300',
                        '2007/01/02 12:34:56.789 +0430',