Skip to content

Commit

Permalink
fix: support generic dataclass (#525)
Browse files Browse the repository at this point in the history
  • Loading branch information
PJCampi authored Apr 26, 2024
1 parent 7d76570 commit de0d230
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 2 deletions.
9 changes: 7 additions & 2 deletions dataclasses_json/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
_get_type_arg_param,
_get_type_args, _is_counter,
_NO_ARGS,
_issubclass_safe, _is_tuple)
_issubclass_safe, _is_tuple,
_is_generic_dataclass)

Json = Union[dict, list, str, int, float, bool, None]

Expand Down Expand Up @@ -269,8 +270,9 @@ def _is_supported_generic(type_):
return False
not_str = not _issubclass_safe(type_, str)
is_enum = _issubclass_safe(type_, Enum)
is_generic_dataclass = _is_generic_dataclass(type_)
return (not_str and _is_collection(type_)) or _is_optional(
type_) or is_union_type(type_) or is_enum
type_) or is_union_type(type_) or is_enum or is_generic_dataclass


def _decode_generic(type_, value, infer_missing):
Expand Down Expand Up @@ -308,6 +310,9 @@ def _decode_generic(type_, value, infer_missing):
except (TypeError, AttributeError):
pass
res = materialize_type(xs)
elif _is_generic_dataclass(type_):
origin = _get_type_origin(type_)
res = _decode_dataclass(origin, value, infer_missing)
else: # Optional or Union
_args = _get_type_args(type_)
if _args is _NO_ARGS:
Expand Down
5 changes: 5 additions & 0 deletions dataclasses_json/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from datetime import datetime, timezone
from collections import Counter
from dataclasses import is_dataclass # type: ignore
from typing import (Collection, Mapping, Optional, TypeVar, Any, Type, Tuple,
Union, cast)

Expand Down Expand Up @@ -164,6 +165,10 @@ def _is_nonstr_collection(type_):
and not _issubclass_safe(type_, str))


def _is_generic_dataclass(type_):
return is_dataclass(_get_type_origin(type_))


def _timestamp_to_dt_aware(timestamp: float):
tz = datetime.now(timezone.utc).astimezone().tzinfo
dt = datetime.fromtimestamp(timestamp, tz=tz)
Expand Down
83 changes: 83 additions & 0 deletions tests/test_generic_dataclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Generic, TypeVar

import pytest

from dataclasses_json import dataclass_json

S = TypeVar("S")
T = TypeVar("T")


@dataclass_json
@dataclass
class Bar:
value: int


@dataclass_json
@dataclass
class Foo(Generic[T]):
bar: T


@dataclass_json
@dataclass
class Baz(Generic[T]):
foo: Foo[T]


@pytest.mark.parametrize(
"instance_of_t, decodes_successfully",
[
pytest.param(1, True, id="literal"),
pytest.param([1], True, id="literal_list"),
pytest.param({"a": 1}, True, id="map_of_literal"),
pytest.param(datetime(2021, 1, 1), False, id="extended_type"),
pytest.param(Bar(1), False, id="object"),
]
)
def test_dataclass_with_generic_dataclass_field(instance_of_t, decodes_successfully):
foo = Foo(bar=instance_of_t)
baz = Baz(foo=foo)
decoded = Baz[type(instance_of_t)].from_json(baz.to_json())
assert decoded.foo == Foo.from_json(foo.to_json())
if decodes_successfully:
assert decoded == baz
else:
assert decoded != baz


@dataclass_json
@dataclass
class Foo2(Generic[T, S]):
bar1: T
bar2: S


@dataclass_json
@dataclass
class Baz2(Generic[T, S]):
foo2: Foo2[T, S]


@pytest.mark.parametrize(
"instance_of_t, decodes_successfully",
[
pytest.param(1, True, id="literal"),
pytest.param([1], True, id="literal_list"),
pytest.param({"a": 1}, True, id="map_of_literal"),
pytest.param(datetime(2021, 1, 1), False, id="extended_type"),
pytest.param(Bar(1), False, id="object"),
]
)
def test_dataclass_with_multiple_generic_dataclass_fields(instance_of_t, decodes_successfully):
foo2 = Foo2(bar1=instance_of_t, bar2=instance_of_t)
baz = Baz2(foo2=foo2)
decoded = Baz2[type(instance_of_t), type(instance_of_t)].from_json(baz.to_json())
assert decoded.foo2 == Foo2.from_json(foo2.to_json())
if decodes_successfully:
assert decoded == baz
else:
assert decoded != baz

6 comments on commit de0d230

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2711794%40–43, 53, 66, 68, 83, 85, 171, 381–388, 392, 398
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py146299%25, 39
   utils.py1342978%13–26, 46–51, 62–66, 76, 101–102, 110–111, 164, 187, 212
tests
   entities.py239399%20, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142299%139–140
   test_str_subclass.py22195%9
   test_union.py159597%274–278
TOTAL277413795% 

Tests Skipped Failures Errors Time
319 1 💤 0 ❌ 0 🔥 2.812s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2711794%40–43, 53, 66, 68, 83, 85, 171, 381–388, 392, 398
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py146299%25, 39
   utils.py1342978%13–26, 46–51, 62–66, 76, 101–102, 110–111, 164, 187, 212
tests
   entities.py239399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142199%139
   test_str_subclass.py22195%9
   test_union.py159597%274–278
TOTAL277413695% 

Tests Skipped Failures Errors Time
319 1 💤 0 ❌ 0 🔥 2.811s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2721794%40–43, 53, 66, 68, 83, 85, 171, 381–388, 392, 398
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py144299%25, 39
   utils.py1342978%13–26, 46–51, 62–66, 76, 101–102, 110–111, 164, 187, 212
tests
   entities.py218399%20, 234, 240
   test_annotations.py804248%50–67, 78–102, 106–122
   test_api.py140299%139–140
   test_str_subclass.py22195%9
   test_union.py134596%274–278
TOTAL264313795% 

Tests Skipped Failures Errors Time
319 1 💤 0 ❌ 0 🔥 2.653s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2721893%40–43, 53, 66, 68, 83, 85, 171, 199, 381–388, 392, 398
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py146299%25, 39
   utils.py1343673%13–26, 46–51, 62–66, 76, 101–102, 110–111, 126–134, 164, 187, 212
tests
   entities.py239399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1591591%99–106, 120–127, 274–278
TOTAL277615894% 

Tests Skipped Failures Errors Time
319 3 💤 0 ❌ 0 🔥 3.206s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2721893%40–43, 53, 66, 68, 83, 85, 171, 199, 381–388, 392, 398
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py146299%25, 39
   utils.py1343673%13–26, 46–51, 62–66, 76, 101–102, 110–111, 126–134, 164, 187, 212
tests
   entities.py239399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1591591%99–106, 120–127, 274–278
TOTAL277615894% 

Tests Skipped Failures Errors Time
319 3 💤 0 ❌ 0 🔥 2.839s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2721893%40–43, 53, 66, 68, 83, 85, 171, 199, 381–388, 392, 398
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py146299%25, 39
   utils.py1343673%13–26, 46–51, 62–66, 76, 101–102, 110–111, 126–134, 164, 187, 212
tests
   entities.py239399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1591591%99–106, 120–127, 274–278
TOTAL277615894% 

Tests Skipped Failures Errors Time
319 3 💤 0 ❌ 0 🔥 4.511s ⏱️

Please sign in to comment.