Repository: ramonhagenaars/jsons

Issue:
Support for `zoneinfo` as specified in Python 3.9
In Python 3.9 `zoneinfo` was [introduced](https://docs.python.org/3/library/zoneinfo.html)`. 

Currently, this simple example code does not work:
```
from zoneinfo import ZoneInfo
import datetime
import jsons

d = datetime.datetime.now(ZoneInfo("America/Los_Angeles"))
jsons.dumps(d)
```

It results in the following error:
```
Traceback (most recent call last):
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_dump_impl.py", line 61, in _do_dump
    result = serializer(obj, cls=cls, **kwargs)
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/serializers/default_datetime.py", line 21, in default_datetime_serializer
    return to_str(obj, strip_microseconds, kwargs['fork_inst'],
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_datetime_impl.py", line 20, in to_str
    offset = get_offset_str(dt, fork_inst)
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_datetime_impl.py", line 37, in get_offset_str
    result = _datetime_offset_str(obj, fork_inst)
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_datetime_impl.py", line 76, in _datetime_offset_str
    tdelta = tzone.utcoffset(None) or tzone.adjusted_offset
AttributeError: 'zoneinfo.ZoneInfo' object has no attribute 'adjusted_offset'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_dump_impl.py", line 88, in dumps
    dumped = dump(obj, *args, **kwargs)
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_dump_impl.py", line 56, in dump
    return _do_dump(obj, serializer, cls, initial, kwargs_)
  File "/Users/***/Library/Python/3.9/lib/python/site-packages/jsons/_dump_impl.py", line 67, in _do_dump
    raise SerializationError(str(err)) from err
jsons.exceptions.SerializationError: 'zoneinfo.ZoneInfo' object has no attribute 'adjusted_offset'
```

I am using `jsons==1.4.0`. Are you planning to support `zoneinfo` soon?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 0ae7d8c..70892fd 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,11 @@ list_of_tuples = jsons.load(some_dict, List[Tuple[AClass, AnotherClass]])
 
 ## Recent updates
 
+### 1.5.0
+
+- Feature: Support for `ZoneInfo` on Python3.9+.
+- Change: microseconds are no longer stripped by default (thanks to pietrodn).
+
 ### 1.4.2
 
 - Bugfix: get_origin did not work with python3.9+ parameterized collections (e.g. `dict[str, str]`).
@@ -161,6 +166,7 @@ list_of_tuples = jsons.load(some_dict, List[Tuple[AClass, AnotherClass]])
 Special thanks to the following contributors of code, discussions or
 suggestions:
 
+[pietrodn](https://github.com/pietrodn),
 [georgeharker](https://github.com/georgeharker),
 [aecay](https://github.com/aecay),
 [bibz](https://github.com/bibz),
diff --git a/jsons/__init__.py b/jsons/__init__.py
index e726b13..7e8118e 100644
--- a/jsons/__init__.py
+++ b/jsons/__init__.py
@@ -152,6 +152,7 @@ from jsons.deserializers.default_timezone import default_timezone_deserializer
 from jsons.deserializers.default_tuple import default_tuple_deserializer
 from jsons.deserializers.default_union import default_union_deserializer
 from jsons.deserializers.default_uuid import default_uuid_deserializer
+from jsons.deserializers.default_zone_info import default_zone_info_deserializer
 from jsons.exceptions import (
     JsonsError,
     ValidationError,
@@ -168,6 +169,7 @@ from jsons.serializers.default_decimal import default_decimal_serializer
 from jsons.serializers.default_dict import default_dict_serializer
 from jsons.serializers.default_enum import default_enum_serializer
 from jsons.serializers.default_iterable import default_iterable_serializer
+from jsons.serializers.default_list import default_list_serializer
 from jsons.serializers.default_object import default_object_serializer
 from jsons.serializers.default_path import default_path_serializer
 from jsons.serializers.default_primitive import default_primitive_serializer
@@ -177,6 +179,7 @@ from jsons.serializers.default_timezone import default_timezone_serializer
 from jsons.serializers.default_tuple import default_tuple_serializer
 from jsons.serializers.default_union import default_union_serializer
 from jsons.serializers.default_uuid import default_uuid_serializer
+from jsons.serializers.default_zone_info import default_zone_info_serializer
 
 KEY_TRANSFORMER_SNAKECASE = snakecase
 KEY_TRANSFORMER_CAMELCASE = camelcase
@@ -232,6 +235,7 @@ __all__ = [
     default_tuple_serializer.__name__,
     default_dict_serializer.__name__,
     default_iterable_serializer.__name__,
+    default_list_serializer.__name__,
     default_enum_serializer.__name__,
     default_complex_serializer.__name__,
     default_datetime_serializer.__name__,
@@ -280,6 +284,7 @@ set_serializer(default_timezone_serializer, timezone)
 set_serializer(default_timedelta_serializer, timedelta)
 set_serializer(default_primitive_serializer, (str, int, float, bool, None))
 set_serializer(default_dict_serializer, Mapping, False)
+set_serializer(default_list_serializer, (list, List))
 set_serializer(default_iterable_serializer, Iterable, False)
 set_serializer(default_object_serializer, object, False)
 set_serializer(default_uuid_serializer, UUID)
@@ -307,3 +312,10 @@ set_deserializer(default_uuid_deserializer, UUID)
 set_deserializer(default_complex_deserializer, complex)
 set_deserializer(default_decimal_deserializer, Decimal)
 set_deserializer(default_path_deserializer, PurePath)
+
+if default_zone_info_serializer and default_zone_info_deserializer:
+    from zoneinfo import ZoneInfo
+    __all__.append(default_zone_info_serializer)
+    __all__.append(default_zone_info_deserializer)
+    set_serializer(default_zone_info_serializer, ZoneInfo)
+    set_deserializer(default_zone_info_deserializer, ZoneInfo)
diff --git a/jsons/_cache.py b/jsons/_cache.py
index 57f039f..0978e24 100644
--- a/jsons/_cache.py
+++ b/jsons/_cache.py
@@ -4,7 +4,7 @@ PRIVATE MODULE: do not import (from) it directly.
 This module contains functionality for caching functions.
 """
 from collections import deque
-from functools import lru_cache
+from functools import lru_cache, update_wrapper
 from typing import Callable
 
 
@@ -32,7 +32,9 @@ def cached(decorated: Callable):
     :param decorated: the decorated function.
     :return: a wrapped function.
     """
-    return _Wrapper(decorated)
+    wrapper = _Wrapper(decorated)
+    update_wrapper(wrapper=wrapper, wrapped=decorated)
+    return wrapper
 
 
 def clear():
diff --git a/jsons/_dump_impl.py b/jsons/_dump_impl.py
index 17373d8..c371657 100644
--- a/jsons/_dump_impl.py
+++ b/jsons/_dump_impl.py
@@ -52,7 +52,6 @@ def dump(obj: object,
         **kwargs
     }
     announce_class(cls_, fork_inst=fork_inst)
-    # kwargs['_objects'].remove(id(obj))
     return _do_dump(obj, serializer, cls, initial, kwargs_)
 
 
diff --git a/jsons/deserializers/default_zone_info.py b/jsons/deserializers/default_zone_info.py
new file mode 100644
index 0000000..fd95e70
--- /dev/null
+++ b/jsons/deserializers/default_zone_info.py
@@ -0,0 +1,18 @@
+from jsons.exceptions import JsonsError
+
+try:
+    from zoneinfo import ZoneInfo
+
+
+    def default_zone_info_deserializer(obj: ZoneInfo, *_, **__) -> ZoneInfo:
+        """
+        Deserialize a ZoneInfo.
+        :param obj: a serialized ZoneInfo object.
+        :return: an instance of ZoneInfo.
+        """
+        if not ZoneInfo:
+            raise JsonsError('ZoneInfo is not available.')
+        return ZoneInfo(obj['key'])
+
+except ImportError:
+    default_zone_info_deserializer = None
diff --git a/jsons/serializers/default_datetime.py b/jsons/serializers/default_datetime.py
index 5330e5e..5f54a5c 100644
--- a/jsons/serializers/default_datetime.py
+++ b/jsons/serializers/default_datetime.py
@@ -6,7 +6,7 @@ from jsons._datetime_impl import to_str, RFC3339_DATETIME_PATTERN
 
 def default_datetime_serializer(obj: datetime,
                                 *,
-                                strip_microseconds: Optional[bool] = True,
+                                strip_microseconds: Optional[bool] = False,
                                 **kwargs) -> str:
     """
     Serialize the given datetime instance to a string. It uses the RFC3339
diff --git a/jsons/serializers/default_list.py b/jsons/serializers/default_list.py
new file mode 100644
index 0000000..fbf6128
--- /dev/null
+++ b/jsons/serializers/default_list.py
@@ -0,0 +1,48 @@
+from typing import Optional
+
+from typish import get_args
+
+from jsons import get_serializer
+from jsons._common_impl import StateHolder
+from jsons._dump_impl import dump
+
+
+def default_list_serializer(
+        obj: list,
+        cls: type = None,
+        *,
+        strict: bool = False,
+        fork_inst: Optional[type] = StateHolder,
+        **kwargs) -> list:
+    """
+    Serialize the given ``obj`` to a list of serialized objects.
+    :param obj: the list that is to be serialized.
+    :param cls: the (subscripted) type of the list.
+    :param strict: a bool to determine if the serializer should be strict
+    (i.e. only dumping stuff that is known to ``cls``).
+    :param fork_inst: if given, it uses this fork of ``JsonSerializable``.
+    :param kwargs: any keyword arguments that may be given to the serialization
+    process.
+    :return: a list of which all elements are serialized.
+    """
+    if not obj:
+        return []
+
+    kwargs_ = {**kwargs, 'strict': strict}
+
+    # The meta kwarg store_cls is filtered out, because an iterable should have
+    # its own -meta attribute.
+    kwargs_.pop('_store_cls', None)
+
+    inner_type = None
+    serializer = dump
+
+    cls_args = get_args(cls)
+    if cls_args:
+        inner_type = cls_args[0]
+        serializer = get_serializer(inner_type, fork_inst)
+    elif strict:
+        inner_type = type(obj[0])
+        serializer = get_serializer(inner_type, fork_inst)
+
+    return [serializer(elem, cls=inner_type, fork_inst=fork_inst, **kwargs_) for elem in obj]
diff --git a/jsons/serializers/default_object.py b/jsons/serializers/default_object.py
index 7b4aa55..dce4188 100644
--- a/jsons/serializers/default_object.py
+++ b/jsons/serializers/default_object.py
@@ -5,11 +5,11 @@ from typing import Optional, Callable, Union, MutableSequence, Tuple, Dict
 
 from typish import get_type, get_mro
 
+from jsons import get_serializer, announce_class
 from jsons._cache import cached
 from jsons._common_impl import get_class_name, META_ATTR, StateHolder
 from jsons._compatibility_impl import get_type_hints
 from jsons._datetime_impl import to_str
-from jsons._dump_impl import dump
 from jsons.classes import JsonSerializable
 from jsons.classes.verbosity import Verbosity
 from jsons.exceptions import SerializationError
@@ -111,26 +111,29 @@ def _do_serialize(
         fork_inst: Optional[type] = StateHolder) -> Dict[str, object]:
     result = dict()
     for attr_name, cls_ in attributes.items():
-        attr = obj.__getattribute__(attr_name)
+        attr = getattr(obj, attr_name)
+        attr_type = cls_ or type(attr)
+        announce_class(attr_type, fork_inst=fork_inst)
+        serializer = get_serializer(attr_type, fork_inst)
         try:
-            dumped_elem = dump(attr,
-                               cls=cls_,
-                               key_transformer=key_transformer,
-                               strip_nulls=strip_nulls,
-                               strip_privates=strip_privates,
-                               strip_properties=strip_properties,
-                               strip_class_variables=strip_class_variables,
-                               strip_attr=strip_attr,
-                               **kwargs)
+            dumped_elem = serializer(attr,
+                                     cls=cls_,
+                                     key_transformer=key_transformer,
+                                     strip_nulls=strip_nulls,
+                                     strip_privates=strip_privates,
+                                     strip_properties=strip_properties,
+                                     strip_class_variables=strip_class_variables,
+                                     strip_attr=strip_attr,
+                                     **kwargs)
             _store_cls_info(dumped_elem, attr, kwargs)
-        except SerializationError as err:
+        except Exception as err:
             if strict:
-                raise
+                raise SerializationError(message=err.args[0]) from err
             else:
                 fork_inst._warn('Failed to dump attribute "{}" of object of '
                                 'type "{}". Reason: {}. Ignoring the '
                                 'attribute.'
-                                .format(attr, get_class_name(cls), err.message),
+                                .format(attr, get_class_name(cls), err.args[0]),
                                 'attribute-not-serialized')
                 break
         _add_dumped_elem(result, attr_name, dumped_elem,
@@ -237,6 +240,7 @@ def _filter_attributes(
             and not _is_innerclass(attr, cls)}
 
 
+@cached
 def _get_class_props(cls: type) -> Tuple[list, list]:
     props = []
     other_cls_vars = []
@@ -313,6 +317,7 @@ def _store_cls_info(result: object, original_obj: dict, kwargs):
         result['-cls'] = cls_name
 
 
+@cached
 def _is_innerclass(attr: str, cls: type) -> bool:
     attr_obj = getattr(cls, attr, None)
     return (isinstance(attr_obj, type)
diff --git a/jsons/serializers/default_zone_info.py b/jsons/serializers/default_zone_info.py
new file mode 100644
index 0000000..7766883
--- /dev/null
+++ b/jsons/serializers/default_zone_info.py
@@ -0,0 +1,15 @@
+from typing import Dict
+
+try:
+    from zoneinfo import ZoneInfo
+
+
+    def default_zone_info_serializer(obj: ZoneInfo, *_, **__) -> Dict[str, str]:
+        """
+        Serialize a ZoneInfo object.
+        :return: a serialized ZoneInfo instance.
+        """
+        return {'key': obj.key}
+
+except ImportError:
+    default_zone_info_serializer = None
diff --git a/setup.py b/setup.py
index ec8cbcf..ac4e15b 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,8 @@ setup(
     python_requires='>=3.5',
     test_suite='tests',
     tests_require=[
-        'dataclasses;python_version=="3.6"'
+        'dataclasses;python_version=="3.6"',
+        'tzdata;python_version>="3.9"'
     ],
     zip_safe=False,
     classifiers=[


Test patch:
diff --git a/tests/test_datetime.py b/tests/test_datetime.py
index 752fa41..e912d0d 100644
--- a/tests/test_datetime.py
+++ b/tests/test_datetime.py
@@ -52,13 +52,13 @@ class TestDatetime(TestCase):
         d = datetime.datetime(year=2018, month=7, day=8, hour=21, minute=34,
                               second=10, microsecond=123456,
                               tzinfo=datetime.timezone.utc)
-        dumped = jsons.dump(d)
+        dumped = jsons.dump(d, strip_microseconds=True)
         self.assertEqual('2018-07-08T21:34:10Z', dumped)
 
     def test_dump_datetime_with_microseconds(self):
         d = datetime.datetime(year=2018, month=7, day=8, hour=21, minute=34,
                               microsecond=123456, tzinfo=datetime.timezone.utc)
-        dumped = jsons.dump(d, strip_microseconds=False)
+        dumped = jsons.dump(d)
         self.assertEqual('2018-07-08T21:34:00.123456Z', dumped)
 
     def test_dump_datetime_with_tz(self):
diff --git a/tests/test_specific_versions.py b/tests/test_specific_versions.py
index f5c2c1d..341df74 100644
--- a/tests/test_specific_versions.py
+++ b/tests/test_specific_versions.py
@@ -52,7 +52,7 @@ class TestSpecificVersions(TestCase):
         self.assertDictEqual(expected, dumped)
 
     @only_version_3(7, and_above=True)
-    def test_postponed_annoation_dataclass(self):
+    def test_postponed_annotation_dataclass(self):
         from postponed_dataclass import Wrap
 
         obj = Wrap()
@@ -145,6 +145,18 @@ class TestSpecificVersions(TestCase):
         dumped2 = h.dump(strict=True)
         self.assertDictEqual(expected2, dumped2)
 
+    @only_version_3(9, and_above=True)
+    def test_postponed_annoation_dataclass(self):
+        # On Python 3.9 ZoneInfo should be available.
+
+        from zoneinfo import ZoneInfo
+
+        info = ZoneInfo(key='America/Los_Angeles')
+        dumped_info = jsons.dump(info)
+        loaded_info = jsons.load(dumped_info, ZoneInfo)
+
+        self.assertEqual(info, loaded_info)
+
     @only_version_3(9, and_above=True)
     def test_dump_load_parameterized_collections(self):
         import version_39