Repository: jsonpickle/jsonpickle (python)

Issue:
Decoding datetime with tzinfo raises TypeError
#### Python Version: 3.10.12

#### jsonpickle Version: 1.2

jsonpickle.decode function is raising `TypeError: 'NoneType' object is not callable` when pass some pickled object containing an datetime object with tzinfo.

datetime object without tzinfo works fine.

``` py
Python 3.10.12 (main, Jun  7 2023, 19:08:47) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> import jsonpickle
>>> 
>>> SaoPaulo = ZoneInfo('America/Sao_Paulo')
>>> 
>>> USEastern = ZoneInfo('US/Eastern')
>>> 
>>> now = datetime.now()
>>> now
datetime.datetime(2023, 6, 8, 15, 27, 1, 305282)
>>> 
>>> now_sp = now.replace(tzinfo=SaoPaulo)
>>> now_us = now.replace(tzinfo=USEastern)
>>> 
>>> pickled_now_sp = jsonpickle.encode(now_sp)
>>> pickled_now_us = jsonpickle.encode(now_us)
>>> pickled_now = jsonpickle.encode(now)
>>> 
>>> unpickled_now = jsonpickle.decode(pickled_now) # This works fine
>>> unpickled_now
datetime.datetime(2023, 6, 8, 15, 27, 1, 305282)
>>>
>>> unpickled_now_sp = jsonpickle.decode(pickled_now_sp) # This raise an error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 41, in decode
    return context.restore(data, reset=reset, classes=classes)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 150, in restore
    value = self._restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 315, in _restore_object
    instance = handler(self).restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/handlers.py", line 170, in restore
    params = (value,) + tuple([restore(i, reset=False) for i in args[1:]])
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/handlers.py", line 170, in <listcomp>
    params = (value,) + tuple([restore(i, reset=False) for i in args[1:]])
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 150, in restore
    value = self._restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 242, in _restore_reduce
    stage1 = f(*args)
TypeError: 'NoneType' object is not callable
>>>
>>>
>>> unpickled_now_us = jsonpickle.decode(pickled_now_us) # diferent tz also raises the same error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 41, in decode
    return context.restore(data, reset=reset, classes=classes)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 150, in restore
    value = self._restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 315, in _restore_object
    instance = handler(self).restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/handlers.py", line 170, in restore
    params = (value,) + tuple([restore(i, reset=False) for i in args[1:]])
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/handlers.py", line 170, in <listcomp>
    params = (value,) + tuple([restore(i, reset=False) for i in args[1:]])
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 150, in restore
    value = self._restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 207, in _restore
    return restore(obj)
  File "/usr/local/python/3.10.12/lib/python3.10/site-packages/jsonpickle/unpickler.py", line 242, in _restore_reduce
    stage1 = f(*args)
TypeError: 'NoneType' object is not callable
```


Pull request:
Fix #447: Decoding zoneinfo
I think I have the fix for issue #447 regarding handling zoneinfo objects. I added a unit test as well, it all passed. 

Patch:
diff --git a/.gitignore b/.gitignore
index 91b9d0c..45cb24e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@
 /.pytest_cache
 /.tox
 /.idea
+/.vscode
 __pycache__
 *.py[cod]
 *.egg-info
diff --git a/CHANGES.rst b/CHANGES.rst
index 3b68255..e680b6a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,8 @@
+v3.0.3
+======
+    * Fixed a bug where pickling some built-in classes (e.g. zoneinfo) 
+      could return a ``None`` module. (#447)
+
 v3.0.2
 ======
     * Properly raise warning if a custom pickling handler returns None. (#433)
diff --git a/jsonpickle/util.py b/jsonpickle/util.py
index 7a86300..c1f88a3 100644
--- a/jsonpickle/util.py
+++ b/jsonpickle/util.py
@@ -517,7 +517,10 @@ def importable_name(cls):
     module = translate_module_name(cls.__module__)
     if not module:
         if hasattr(cls, '__self__'):
-            module = cls.__self__.__class__.__module__
+            if hasattr(cls.__self__, '__module__'):
+                module = cls.__self__.__module__
+            else:
+                module = cls.__self__.__class__.__module__
     return '{}.{}'.format(module, name)
 
 


Test patch:
diff --git a/tests/datetime_test.py b/tests/datetime_test.py
index e53478b..300ae03 100644
--- a/tests/datetime_test.py
+++ b/tests/datetime_test.py
@@ -8,6 +8,7 @@
 
 import datetime
 import time
+import sys
 import unittest
 
 import jsonpickle
@@ -167,6 +168,25 @@ class DateTimeSimpleTestCase(unittest.TestCase):
         test_obj_decoded = jsonpickle.decode(json)
         self.assertEqual(test_obj_decoded.data['ts'], test_obj_decoded.data_ref['ts'])
 
+    @unittest.skipIf(sys.version_info < (3, 9), "only tested for python >= 3.9")
+    def test_datetime_with_ZoneInfo(self):
+        """
+        jsonpickle should pickle a datetime object with time zone info
+        """
+        # if sys.version_info >= (3, 9):
+        from zoneinfo import ZoneInfo
+
+        now = datetime.datetime.now()
+
+        SaoPaulo = ZoneInfo('America/Sao_Paulo')
+        USEastern = ZoneInfo('US/Eastern')
+
+        now_sp = now.replace(tzinfo=SaoPaulo)
+        now_us = now.replace(tzinfo=USEastern)
+
+        self._roundtrip(now_sp)
+        self._roundtrip(now_us)
+
 
 class DateTimeAdvancedTestCase(unittest.TestCase):
     def setUp(self):
diff --git a/tests/zoneinfo_test.py b/tests/zoneinfo_test.py
new file mode 100644
index 0000000..1c0fdcb
--- /dev/null
+++ b/tests/zoneinfo_test.py
@@ -0,0 +1,32 @@
+import sys
+
+if sys.version_info >= (3, 9):
+    from zoneinfo import ZoneInfo
+
+    import unittest
+
+    import jsonpickle
+
+    class ZoneInfoSimpleTestCase(unittest.TestCase):
+        def _roundtrip(self, obj):
+            """
+            pickle and then unpickle object, then assert the new object is the
+            same as the original.
+            """
+            pickled = jsonpickle.encode(obj)
+            unpickled = jsonpickle.decode(pickled)
+            self.assertEqual(obj, unpickled)
+
+        def test_zoneinfo(self):
+            """
+            jsonpickle should pickle a zoneinfo object
+            """
+            self._roundtrip(ZoneInfo("Australia/Queensland"))
+
+    def suite():
+        suite = unittest.TestSuite()
+        suite.addTest(unittest.makeSuite(ZoneInfoSimpleTestCase))
+        return suite
+
+    if __name__ == '__main__':
+        unittest.main(defaultTest='suite')