Skip to content

Commit

Permalink
fix: apply global config globally (#524)
Browse files Browse the repository at this point in the history
  • Loading branch information
PJCampi committed Apr 25, 2024
1 parent 8512afc commit 7d76570
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 15 deletions.
48 changes: 33 additions & 15 deletions dataclasses_json/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ def _decode_dataclass(cls, kvs, infer_missing):
return cls(**init_kwargs)


def _decode_type(type_, value, infer_missing):
if _has_decoder_in_global_config(type_):
return _get_decoder_in_global_config(type_)(value)
if is_dataclass(type_) or is_dataclass(type_):
return _decode_dataclass(type_, value, infer_missing)
if _is_supported_generic(type_):
return _decode_generic(type_, value, infer_missing)
return _support_extended_types(type_, value)


def _support_extended_types(field_type, field_value):
if _issubclass_safe(field_type, datetime):
# FIXME this is a hack to deal with mm already decoding
Expand Down Expand Up @@ -305,12 +315,7 @@ def _decode_generic(type_, value, infer_missing):
res = value
elif _is_optional(type_) and len(_args) == 2: # Optional
type_arg = _get_type_arg_param(type_, 0)
if is_dataclass(type_arg) or is_dataclass(value):
res = _decode_dataclass(type_arg, value, infer_missing)
elif _is_supported_generic(type_arg):
res = _decode_generic(type_arg, value, infer_missing)
else:
res = _support_extended_types(type_arg, value)
res = _decode_type(type_arg, value, infer_missing)
else: # Union (already decoded or try to decode a dataclass)
type_options = _get_type_args(type_)
res = value # assume already decoded
Expand Down Expand Up @@ -367,13 +372,6 @@ def _decode_items(type_args, xs, infer_missing):
type_arg is a typevar we need to extract the reified type information
hence the check of `is_dataclass(vs)`
"""
def _decode_item(type_arg, x):
if is_dataclass(type_arg) or is_dataclass(xs):
return _decode_dataclass(type_arg, x, infer_missing)
if _is_supported_generic(type_arg):
return _decode_generic(type_arg, x, infer_missing)
return x

def handle_pep0673(pre_0673_hint: str) -> Union[Type, str]:
for module in sys.modules:
maybe_resolved = getattr(sys.modules[module], type_args, None)
Expand All @@ -390,13 +388,13 @@ def handle_pep0673(pre_0673_hint: str) -> Union[Type, str]:

if _isinstance_safe(type_args, Collection) and not _issubclass_safe(type_args, Enum):
if len(type_args) == len(xs):
return list(_decode_item(type_arg, x) for type_arg, x in zip(type_args, xs))
return list(_decode_type(type_arg, x, infer_missing) for type_arg, x in zip(type_args, xs))
else:
raise TypeError(f"Number of types specified in the collection type {str(type_args)} "
f"does not match number of elements in the collection. In case you are working with tuples"
f"take a look at this document "
f"docs.python.org/3/library/typing.html#annotating-tuples.")
return list(_decode_item(type_args, x) for x in xs)
return list(_decode_type(type_args, x, infer_missing) for x in xs)


def _asdict(obj, encode_json=False):
Expand Down Expand Up @@ -428,5 +426,25 @@ def _asdict(obj, encode_json=False):
# enum.IntFlag and enum.Flag are regarded as collections in Python 3.11, thus a check against Enum is needed
elif isinstance(obj, Collection) and not isinstance(obj, (str, bytes, Enum)):
return list(_asdict(v, encode_json=encode_json) for v in obj)
# encoding of generics primarily relies on concrete types while decoding relies on type annotations. This makes
# applying encoders/decoders from global configuration inconsistent.
elif _has_encoder_in_global_config(type(obj)):
return _get_encoder_in_global_config(type(obj))(obj)
else:
return copy.deepcopy(obj)


def _has_decoder_in_global_config(type_):
return type_ in cfg.global_config.decoders


def _get_decoder_in_global_config(type_):
return cfg.global_config.decoders[type_]


def _has_encoder_in_global_config(type_):
return type_ in cfg.global_config.encoders


def _get_encoder_in_global_config(type_):
return cfg.global_config.encoders[type_]
33 changes: 33 additions & 0 deletions tests/test_global_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import List, Optional

from dataclasses_json import dataclass_json
from datetime import date
Expand All @@ -19,6 +20,18 @@ class PersonWithBirthday:
birthday: date


@dataclass_json
@dataclass
class HistoricalEvents:
dates: List[date]


@dataclass_json
@dataclass
class PackageDelivery:
date: Optional[date]


class TestGlobalConfig:
def test_encoder_override(self):
dataclasses_json.cfg.global_config.encoders[str] = lambda s: s[::-1]
Expand All @@ -30,3 +43,23 @@ def test_encoder_extension(self):
assert PersonWithBirthday("Kobe Bryant", date(1978, 8, 23)).to_json() \
== '{"name": "Kobe Bryant", "birthday": "1978-08-23"}'
dataclasses_json.cfg.global_config.encoders = {}

def test_encoder_and_decoder_extension_in_collections(self):
dataclasses_json.cfg.global_config.encoders[date] = date.isoformat
dataclasses_json.cfg.global_config.decoders[date] = date.fromisoformat
historical_events = HistoricalEvents([date(1918, 11, 11), date(1945, 5, 8)])
expected_json = '{"dates": ["1918-11-11", "1945-05-08"]}'
assert historical_events.to_json() == expected_json
assert HistoricalEvents.from_json(expected_json) == historical_events
dataclasses_json.cfg.global_config.encoders = {}
dataclasses_json.cfg.global_config.decoders = {}

def test_encoder_and_decoder_extension_in_union(self):
dataclasses_json.cfg.global_config.encoders[date] = date.isoformat
dataclasses_json.cfg.global_config.decoders[date] = date.fromisoformat
package_delivery = PackageDelivery(date(2023, 1, 1))
expected_json = '{"date": "2023-01-01"}'
assert package_delivery.to_json() == expected_json
assert PackageDelivery.from_json(expected_json) == package_delivery
dataclasses_json.cfg.global_config.encoders = {}
dataclasses_json.cfg.global_config.decoders = {}
8 changes: 8 additions & 0 deletions tests/test_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,11 @@ def test_deserialize_with_mismatched_field_types():
obj = s.loads(json)
assert obj.event is not None
assert obj.event.data == "Hello world!"


def test_deserialize_with_mismatched_field_types():
json = '{"event": {"data": "Hello world!"} }'
s = C16.schema()
obj = s.loads(json)
assert obj.event is not None
assert obj.event.data == "Hello world!"

6 comments on commit 7d76570

@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.py2671794%39–42, 52, 65, 67, 82, 84, 170, 376–383, 387, 393
   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.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
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
TOTAL272113695% 

Tests Skipped Failures Errors Time
309 1 💤 0 ❌ 0 🔥 2.971s ⏱️

@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.py2671794%39–42, 52, 65, 67, 82, 84, 170, 376–383, 387, 393
   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.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
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
TOTAL272113795% 

Tests Skipped Failures Errors Time
309 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.py2681794%39–42, 52, 65, 67, 82, 84, 170, 376–383, 387, 393
   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.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
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
TOTAL259313795% 

Tests Skipped Failures Errors Time
309 1 💤 0 ❌ 0 🔥 2.755s ⏱️

@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.py2681893%39–42, 52, 65, 67, 82, 84, 170, 198, 376–383, 387, 393
   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.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
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
TOTAL272315894% 

Tests Skipped Failures Errors Time
309 3 💤 0 ❌ 0 🔥 2.795s ⏱️

@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.py2681893%39–42, 52, 65, 67, 82, 84, 170, 198, 376–383, 387, 393
   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.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
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
TOTAL272315894% 

Tests Skipped Failures Errors Time
309 3 💤 0 ❌ 0 🔥 2.570s ⏱️

@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.py2681893%39–42, 52, 65, 67, 82, 84, 170, 198, 376–383, 387, 393
   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.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
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
TOTAL272315894% 

Tests Skipped Failures Errors Time
309 3 💤 0 ❌ 0 🔥 4.622s ⏱️

Please sign in to comment.