Skip to content

Commit

Permalink
Merge branch 'matthew-chambers-pushly-master'
Browse files Browse the repository at this point in the history
  • Loading branch information
Fatal1ty committed Nov 14, 2023
2 parents 14b9389 + 3c16317 commit 23d38c8
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 10 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Table of contents
* [`discriminator` config option](#discriminator-config-option)
* [`lazy_compilation` config option](#lazy_compilation-config-option)
* [`sort_keys` config option](#sort_keys-config-option)
* [`allow_deserialization_not_by_alias` config option](#allow_deserialization_not_by_alias-config-option)
* [Passing field values as is](#passing-field-values-as-is)
* [Extending existing types](#extending-existing-types)
* [Dialects](#dialects)
Expand Down Expand Up @@ -1391,6 +1392,39 @@ t = SortedDataClass(1, 2)
assert t.to_dict() == {"bar": 2, "foo": 1}
```
#### `allow_deserialization_not_by_alias` config option

When using aliases, the deserializer defaults to requiring the keys to match
what is defined as the alias.
If the flexibility to deserialize aliased and unaliased keys is required then
the config option `allow_deserialization_not_by_alias = True` can be set to
enable the feature.

```python
from dataclasses import dataclass, field
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


@dataclass
class AliasedDataClass(DataClassDictMixin):
foo: int = field(metadata={"alias": "alias_foo"})
bar: int = field(metadata={"alias": "alias_bar"})

class Config(BaseConfig):
allow_deserialization_not_by_alias = True


alias_dict = {"alias_foo": 1, "alias_bar": 2}
t1 = AliasedDataClass.from_dict(alias_dict)

no_alias_dict = {"foo": 1, "bar": 2}
# This would raise `mashumaro.exceptions.MissingField`
# if allow_deserialization_not_by_alias was False
t2 = AliasedDataClass.from_dict(no_alias_dict)
assert t1 == t2
```

### Passing field values as is

In some cases it's needed to pass a field value as is without any changes
Expand Down
1 change: 1 addition & 0 deletions mashumaro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ class BaseConfig:
discriminator: Optional[Discriminator] = None
lazy_compilation: bool = False
sort_keys: bool = False
allow_deserialization_not_by_alias: bool = False
38 changes: 29 additions & 9 deletions mashumaro/core/meta/code/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,16 +555,36 @@ def _unpack_method_set_value(
could_be_none=False if could_be_none else True,
)
)
if unpacked_value != "value":
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
elif has_default:
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
if self.get_config().allow_deserialization_not_by_alias:
if unpacked_value != "value":
self.add_line(f"value = d.get('{alias}', MISSING)")
with self.indent("if value is MISSING:"):
self.add_line(f"value = d.get('{fname}', MISSING)")
packed_value = "value"
elif has_default:
self.add_line(f"value = d.get('{alias}', MISSING)")
with self.indent("if value is MISSING:"):
self.add_line(f"value = d.get('{fname}', MISSING)")
packed_value = "value"
else:
self.add_line(f"__{fname} = d.get('{alias}', MISSING)")
with self.indent(f"if __{fname} is MISSING:"):
self.add_line(f"__{fname} = d.get('{fname}', MISSING)")
packed_value = f"__{fname}"
unpacked_value = packed_value
else:
self.add_line(f"__{fname} = d.get('{alias or fname}', MISSING)")
packed_value = f"__{fname}"
unpacked_value = packed_value
if unpacked_value != "value":
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
elif has_default:
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
else:
self.add_line(
f"__{fname} = d.get('{alias or fname}', MISSING)"
)
packed_value = f"__{fname}"
unpacked_value = packed_value
if not has_default:
with self.indent(f"if {packed_value} is MISSING:"):
self.add_line(
Expand Down
47 changes: 47 additions & 0 deletions tests/test_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,50 @@ class Config(BaseConfig):

assert DataClass(x=123).to_dict() == {"alias": 123}
assert DataClass(x=None).to_dict() == {"alias": None}


def test_by_field_with_loose_deserialize():
@dataclass
class DataClass(DataClassDictMixin):
a: int = field(metadata={"alias": "alias_a"})
b: Optional[int] = field(metadata={"alias": "alias_b"})
c: Optional[str] = field(metadata={"alias": "alias_c"})
d: int = field(metadata={"alias": "alias_d"}, default=4)
e: Optional[int] = field(metadata={"alias": "alias_e"}, default=5)
f: Optional[str] = field(metadata={"alias": "alias_f"}, default="6")

class Config(BaseConfig):
serialize_by_alias = True
code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]
allow_deserialization_not_by_alias = True

instance = DataClass(a=1, b=2, c="3")
assert (
DataClass.from_dict(
{
"a": 1,
"alias_b": 2,
"c": "3",
"alias_d": 4,
"e": 5,
"alias_f": "6",
}
)
== instance
)
assert instance.to_dict() == {
"alias_a": 1,
"alias_b": 2,
"alias_c": "3",
"alias_d": 4,
"alias_e": 5,
"alias_f": "6",
}
assert instance.to_dict(by_alias=False) == {
"a": 1,
"b": 2,
"c": "3",
"d": 4,
"e": 5,
"f": "6",
}
2 changes: 1 addition & 1 deletion tests/test_data_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from typing_extensions import Final, LiteralString

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.config import BaseConfig, TO_DICT_ADD_BY_ALIAS_FLAG
from mashumaro.core.const import PEP_585_COMPATIBLE, PY_39_MIN
from mashumaro.exceptions import (
InvalidFieldValue,
Expand Down

0 comments on commit 23d38c8

Please sign in to comment.