diff --git a/HISTORY.rst b/HISTORY.rst index 8b374ed6..97ba2123 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,22 @@ History ======= +0.32.0 (2024-11-30) +------------------- + +**Features and Improvements** + +- Add support for ``Sequence`` and ``MutableSequence`` + +**Bugfixes** + +* Fix a bug in ``ParseError`` +* Fix issue when field in ``EnvWizard`` is a dataclass, and + instance of that type is passed to constructor +* Fix some logic in ``parsers.py`` that was relying on all parsers + being subclass of :class:`AbstractParser`, as they can now be a function + instead. + 0.31.0 (2024-11-30) ------------------- diff --git a/dataclass_wizard/errors.py b/dataclass_wizard/errors.py index 65d5fec7..8ff8248e 100644 --- a/dataclass_wizard/errors.py +++ b/dataclass_wizard/errors.py @@ -145,7 +145,6 @@ def message(self) -> str: obj_type=self.name(self.obj_type)) if self.json_object: - self.kwargs['json_object'] = json.dumps(self.json_object, default=str) from .utils.json_util import safe_dumps self.kwargs['json_object'] = safe_dumps(self.json_object) @@ -297,7 +296,6 @@ def message(self) -> str: msg = self._TEMPLATE.format( cls=self.class_name, - # json_string=json.dumps(self.obj, default=str), json_string=safe_dumps(self.obj), fields=self.fields, json_key=self.json_key) @@ -335,7 +333,6 @@ def message(self) -> str: msg = self._TEMPLATE.format( cls=self.class_name, nested_cls=self.nested_class_name, - # json_string=json.dumps(self.obj, default=str), json_string=safe_dumps(self.obj), field=self.field_name, o=self.obj, diff --git a/dataclass_wizard/loaders.py b/dataclass_wizard/loaders.py index 7f920e56..651da111 100644 --- a/dataclass_wizard/loaders.py +++ b/dataclass_wizard/loaders.py @@ -10,6 +10,8 @@ NamedTupleMeta, SupportsFloat, AnyStr, Text, Callable, Optional ) +from collections.abc import Sequence as ABCSequence, MutableSequence as ABCMutableSequence + from uuid import UUID from .abstractions import AbstractLoader, AbstractParser @@ -438,6 +440,18 @@ def get_parser_for_annotation(cls, ann_type: Type[T], cls.get_parser_for_annotation ) + elif base_type in (ABCSequence, ABCMutableSequence): + + load_hook = cls.load_to_iterable + # Re-map to list, e.g. `Sequence[int]` -> `list[int]` + ann_type = list[ann_type] if ( + ann_type := get_args(ann_type)[0]) else list + + return IterableParser( + base_cls, extras, ann_type, load_hook, + cls.get_parser_for_annotation + ) + else: load_hook = hooks.get(base_type) diff --git a/dataclass_wizard/parsers.py b/dataclass_wizard/parsers.py index 8e6cabef..e5639a65 100644 --- a/dataclass_wizard/parsers.py +++ b/dataclass_wizard/parsers.py @@ -364,7 +364,8 @@ class TupleParser(AbstractParser[Type[S], S]): __slots__ = ('hook', 'elem_parsers', 'total_count', - 'required_count') + 'required_count', + 'elem_types') # Base type of the object which is instantiable # ex. `Tuple[bool, int]` -> `tuple` @@ -378,7 +379,7 @@ def __post_init__(self, cls: Type, # Get the subscripted values # ex. `Tuple[bool, int]` -> (bool, int) - elem_types = get_args(self.base_type) + self.elem_types = elem_types = get_args(self.base_type) self.base_type = get_origin(self.base_type) # A collection with a parser for each type argument elem_parsers = tuple(get_parser(t, cls, extras) @@ -391,7 +392,8 @@ def __post_init__(self, cls: Type, # this should exclude the parsers for `Optional` or `Union` types # that have `None` in the list of args. self.required_count: int = len(tuple(p for p in elem_parsers - if None not in p)) + if not isinstance(p, AbstractParser) + or None not in p)) self.elem_parsers = elem_parsers or None @@ -410,8 +412,9 @@ def __call__(self, o: S) -> S: desired_count = str(self.total_count) # self.elem_parsers can be None at this moment - elem_parsers_types = [p.base_type for p in self.elem_parsers] \ - if self.elem_parsers else [] + elem_parsers_types = [getattr(p, 'base_type', tp) for p, tp in + zip(self.elem_parsers, self.elem_types)] \ + if self.elem_parsers else self.elem_types raise ParseError( e, o, elem_parsers_types,