Skip to content

Commit

Permalink
completely fix AliasPath
Browse files Browse the repository at this point in the history
  • Loading branch information
rnag committed Dec 17, 2024
1 parent 492f84c commit 9044407
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 20 deletions.
31 changes: 23 additions & 8 deletions dataclass_wizard/class_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@
# A cached mapping, per dataclass, of instance field name to JSON path
DATACLASS_FIELD_TO_JSON_PATH = defaultdict(dict)

# V1 Load: A cached mapping, per dataclass, of instance field name to JSON field
# V1 Load: A cached mapping, per dataclass, of instance field name to alias path
DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD = defaultdict(dict)

# V1 Load: A cached mapping, per dataclass, of instance field name to alias
DATACLASS_FIELD_TO_ALIAS_FOR_LOAD = defaultdict(dict)

# A cached mapping, per dataclass, of instance field name to JSON field
Expand Down Expand Up @@ -324,17 +327,24 @@ def v1_dataclass_field_to_alias(
def _process_field(name: str,
f: Field,
set_paths: bool,
dataclass_field_to_path,
load_dataclass_field_to_path,
dump_dataclass_field_to_path,
load_dataclass_field_to_alias,
dump_dataclass_field_to_alias):
"""Process a :class:`Field` for a dataclass field."""

if f.path is not None:
if set_paths and f.load_alias is not ExplicitNull:
dataclass_field_to_path[name] = f.path
if f.dump_alias is not ExplicitNull:
value = ExplicitNull if f.skip else ''
dump_dataclass_field_to_alias[name] = value
if set_paths:
if f.load_alias is not ExplicitNull:
load_dataclass_field_to_path[name] = f.path
if not f.skip and f.dump_alias is not ExplicitNull:
dump_dataclass_field_to_path[name] = f.path
# TODO I forget why this is needed :o
if f.skip:
dump_dataclass_field_to_alias[name] = ExplicitNull
elif f.dump_alias is not ExplicitNull:
dump_dataclass_field_to_alias[name] = ''

else:
if f.load_alias is not None:
load_dataclass_field_to_alias[name] = f.load_alias
Expand All @@ -354,7 +364,9 @@ def _setup_v1_load_config_for_cls(
load_dataclass_field_to_alias = DATACLASS_FIELD_TO_ALIAS_FOR_LOAD[cls]
dump_dataclass_field_to_alias = DATACLASS_FIELD_TO_ALIAS[cls]

dataclass_field_to_path = DATACLASS_FIELD_TO_JSON_PATH[cls]
dataclass_field_to_path = DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD[cls]
dump_dataclass_field_to_path = DATACLASS_FIELD_TO_JSON_PATH[cls]

set_paths = False if dataclass_field_to_path else True

for f in dataclass_init_fields(cls):
Expand All @@ -369,6 +381,7 @@ def _setup_v1_load_config_for_cls(
if isinstance(f, Field):
_process_field(f.name, f, set_paths,
dataclass_field_to_path,
dump_dataclass_field_to_path,
load_dataclass_field_to_alias,
dump_dataclass_field_to_alias)

Expand All @@ -377,6 +390,7 @@ def _setup_v1_load_config_for_cls(
if isinstance(value, Field):
_process_field(f.name, value, set_paths,
dataclass_field_to_path,
dump_dataclass_field_to_path,
load_dataclass_field_to_alias,
dump_dataclass_field_to_alias)

Expand All @@ -396,6 +410,7 @@ def _setup_v1_load_config_for_cls(
if isinstance(extra, Field):
_process_field(f.name, extra, set_paths,
dataclass_field_to_path,
dump_dataclass_field_to_path,
load_dataclass_field_to_alias,
dump_dataclass_field_to_alias)
# elif isinstance(extra, PatternedDT):
Expand Down
5 changes: 4 additions & 1 deletion dataclass_wizard/class_helper.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ JSON_FIELD_TO_DATACLASS_FIELD: dict[type, dict[str, str | ExplicitNullType]] = d
# A cached mapping, per dataclass, of instance field name to JSON path
DATACLASS_FIELD_TO_JSON_PATH: dict[type, dict[str, PathType]] = defaultdict(dict)

# A cached mapping, per dataclass, of instance field name to JSON field
# V1: A cached mapping, per dataclass, of instance field name to JSON path
DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD: dict[type, dict[str, PathType]] = defaultdict(dict)

# V1: A cached mapping, per dataclass, of instance field name to JSON field
DATACLASS_FIELD_TO_ALIAS_FOR_LOAD: dict[type, dict[str, str]] = defaultdict(dict)

# A cached mapping, per dataclass, of instance field name to JSON field
Expand Down
4 changes: 2 additions & 2 deletions dataclass_wizard/v1/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ..bases import BaseLoadHook, AbstractMeta
from ..class_helper import (
v1_dataclass_field_to_alias, json_field_to_dataclass_field,
CLASS_TO_LOAD_FUNC, dataclass_fields, get_meta, is_subclass_safe, dataclass_field_to_json_path,
CLASS_TO_LOAD_FUNC, dataclass_fields, get_meta, is_subclass_safe, DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD,
dataclass_init_fields, dataclass_field_to_default, create_meta, dataclass_init_field_names,
)
from ..constants import CATCH_ALL, TAG
Expand Down Expand Up @@ -918,7 +918,7 @@ def load_func_for_dataclass(
# else:
# raise RecursiveClassError(cls) from None

field_to_path = dataclass_field_to_json_path(cls)
field_to_path = DATACLASS_FIELD_TO_ALIAS_PATH_FOR_LOAD[cls]
has_alias_paths = True if field_to_path else False

# Fix for using `auto_assign_tags` and `raise_on_unknown_json_key` together
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/environ/test_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class MyClass(EnvWizard, reload_env=True):
inner_cls_2: Inner2

c = MyClass()
print(c)
# print(c)

assert c.dict() == {
'inner_cls_1': Inner1(my_bool=False,
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_wizard_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ class MyClass(TOMLWizard, key_transform='SNAKE'):
MyClass('testing!', {'333': 'this is a test.'})
])

print(toml_string)
# print(toml_string)

assert toml_string == """\
items = [
Expand Down
56 changes: 49 additions & 7 deletions tests/unit/v1/test_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2417,10 +2417,10 @@ class _(JSONWizard.Meta):
assert opt == Options(my_extras={}, email='[email protected]')


def test_from_dict_with_nested_object_key_path():
def test_from_dict_with_nested_object_alias_path():
"""
Specifying a custom mapping of "nested" JSON key to dataclass field,
via the `KeyPath` and `path_field` helper functions.
Specifying a custom mapping of "nested" alias to dataclass field,
via the `AliasPath` helper function.
"""

@dataclass
Expand Down Expand Up @@ -2507,12 +2507,12 @@ class _(JSONPyWizard.Meta):
}


def test_from_dict_with_nested_object_key_path_with_skip_defaults():
def test_from_dict_with_nested_object_alias_path_with_skip_defaults():
"""
Specifying a custom mapping of "nested" JSON key to dataclass field,
via the `KeyPath` and `path_field` helper functions.
Specifying a custom mapping of "nested" alias to dataclass field,
via the `AliasPath` helper function.
Test with `skip_defaults=True` and `dump=False`.
Test with `skip_defaults=True`, `load_alias`, and `skip=True`.
"""

@dataclass
Expand Down Expand Up @@ -2618,6 +2618,48 @@ class _(JSONWizard.Meta):
}


def test_from_dict_with_nested_object_alias_path_with_dump_alias_and_skip():
"""
Test nested object `AliasPath` with dump='...' and skip=True,
along with `Alias` with `skip=True`,
added for branch coverage.
"""
@dataclass
class A(JSONWizard):

class _(JSONWizard.Meta):
v1 = True

my_str: str = AliasPath(dump='a.b.c[0]')
my_bool: bool = AliasPath('x.y."Z 1"', skip=True)
my_int: int = Alias('my Integer', skip=True)

d = {'a': {'b': {'c': [1, 2, 3]}},
'x': {'y': {'Z 1': 'f'}},}

with pytest.raises(MissingFields) as exc_info:
_ = A.from_dict(d)

e = exc_info.value
assert e.fields == ['my_bool']
assert e.missing_fields == ['my_str', 'my_int']

d = {'my_str': 'test',
'my Integer': '123',
'x': {'y': {'Z 1': 'f'}},}

a = A.from_dict(d)

assert a.my_str == 'test'
assert a.my_int == 123
assert a.my_bool is False

serialized = a.to_dict()
assert serialized == {
'a': {'b': {'c': {0: 'test'}}},
}


def test_auto_assign_tags_and_raise_on_unknown_json_key():

@dataclass
Expand Down

0 comments on commit 9044407

Please sign in to comment.