Skip to content

Commit

Permalink
Merge pull request #162 from Fatal1ty/omit-default
Browse files Browse the repository at this point in the history
Add omit_default config and dialect option
  • Loading branch information
Fatal1ty committed Sep 8, 2023
2 parents 92d539c + b6af297 commit 75dbebc
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 20 deletions.
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Table of contents
* [`aliases` config option](#aliases-config-option)
* [`serialize_by_alias` config option](#serialize_by_alias-config-option)
* [`omit_none` config option](#omit_none-config-option)
* [`omit_default` config option](#omit_default-config-option)
* [`namedtuple_as_dict` config option](#namedtuple_as_dict-config-option)
* [`allow_postponed_evaluation` config option](#allow_postponed_evaluation-config-option)
* [`dialect` config option](#dialect-config-option)
Expand All @@ -70,6 +71,7 @@ Table of contents
* [Dialects](#dialects)
* [`serialization_strategy` dialect option](#serialization_strategy-dialect-option)
* [`omit_none` dialect option](#omit_none-dialect-option)
* [`omit_default` dialect option](#omit_default-dialect-option)
* [Changing the default dialect](#changing-the-default-dialect)
* [Discriminator](#discriminator)
* [Subclasses distinguishable by a field](#subclasses-distinguishable-by-a-field)
Expand Down Expand Up @@ -1159,19 +1161,47 @@ default when this option is enabled. You can mix this config option with
[`omit_none`](#add-omit_none-keyword-argument) keyword argument.

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

@dataclass
class DataClass(DataClassDictMixin):
x: Optional[int] = None
x: Optional[int] = 42

class Config(BaseConfig):
omit_none = True

DataClass().to_dict() # {}
DataClass(x=None).to_dict() # {}
```

#### `omit_default` config option

When this option enabled, all the fields that have values equal to the defaults
or the default_factory results will be excluded during serialization.

```python
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
from mashumaro import DataClassDictMixin, field_options
from mashumaro.config import BaseConfig

@dataclass
class Foo:
foo: str

@dataclass
class DataClass(DataClassDictMixin):
a: int = 42
b: Tuple[int, ...] = field(default=(1, 2, 3))
c: List[Foo] = field(default_factory=lambda: [Foo("foo")])
d: Optional[str] = None

class Config(BaseConfig):
omit_default = True

DataClass(a=42, b=(1, 2, 3), c=[Foo("foo")]).to_dict() # {}
```

#### `namedtuple_as_dict` config option
Expand Down Expand Up @@ -1532,6 +1562,11 @@ but for the dialect scope. You can register custom [`SerializationStrategy`](#se
This dialect option has the same meaning as the
[similar config option](#omit_none-config-option) but for the dialect scope.
#### `omit_default` dialect option
This dialect option has the same meaning as the
[similar config option](#omitdefault-config-option) but for the dialect scope.
#### Changing the default dialect
You can change the default serialization and deserialization methods for
Expand Down
1 change: 1 addition & 0 deletions mashumaro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class BaseConfig:
allow_postponed_evaluation: bool = True
dialect: Optional[Type[Dialect]] = None
omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
omit_default: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
orjson_options: Optional[int] = 0
json_schema: Dict[str, Any] = {}
discriminator: Optional[Discriminator] = None
Expand Down
105 changes: 91 additions & 14 deletions mashumaro/core/meta/code/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import types
import typing
import uuid
from contextlib import contextmanager

# noinspection PyProtectedMember
Expand Down Expand Up @@ -43,13 +44,14 @@
is_hashable,
is_init_var,
is_literal,
is_named_tuple,
is_optional,
is_type_var_any,
resolve_type_params,
substitute_type_params,
type_name,
)
from mashumaro.core.meta.types.common import FieldContext, ValueSpec
from mashumaro.core.meta.types.common import FieldContext, NoneType, ValueSpec
from mashumaro.core.meta.types.pack import PackerRegistry
from mashumaro.core.meta.types.unpack import (
SubtypeUnpackerBuilder,
Expand Down Expand Up @@ -215,14 +217,19 @@ def metadatas(self) -> typing.Dict[str, typing.Mapping[str, typing.Any]]:
# https://github.com/python/mypy/issues/1362
}

def get_field_default(self, name: str) -> typing.Any:
def get_field_default(
self, name: str, call_factory: bool = False
) -> typing.Any:
field = self.dataclass_fields.get(name) # type: ignore
# https://github.com/python/mypy/issues/1362
if field:
if field.default is not MISSING:
return field.default
else:
return field.default_factory
if call_factory and field.default_factory is not MISSING:
return field.default_factory()
else:
return field.default_factory
else:
return self.namespace.get(name, MISSING)

Expand Down Expand Up @@ -691,9 +698,7 @@ def get_pack_method_default_flag_values(
TO_DICT_ADD_OMIT_NONE_FLAG, cls
)
if omit_none_feature:
omit_none = self._get_dialect_or_config_option(
"omit_none", False, None
)
omit_none = self._get_dialect_or_config_option("omit_none", False)
kw_param_names.append("omit_none")
kw_param_values.append("True" if omit_none else "False")

Expand Down Expand Up @@ -856,6 +861,10 @@ def _add_pack_method_lines(self, method_name: str) -> None:
)
serialize_by_alias = self.get_config().serialize_by_alias
omit_none = self._get_dialect_or_config_option("omit_none", False)
omit_default = self._get_dialect_or_config_option(
"omit_default", False
)
force_value = omit_default
packers = {}
aliases = {}
nullable_fields = set()
Expand All @@ -864,7 +873,7 @@ def _add_pack_method_lines(self, method_name: str) -> None:
if self.metadatas.get(fname, {}).get("serialize") == "omit":
continue
packer, alias, could_be_none = self._get_field_packer(
fname, ftype, config
fname, ftype, config, force_value
)
packers[fname] = packer
if alias:
Expand All @@ -879,41 +888,73 @@ def _add_pack_method_lines(self, method_name: str) -> None:
and (omit_none or omit_none_feature)
or by_alias_feature
and aliases
or omit_default
):
kwargs = "kwargs"
self.add_line("kwargs = {}")
for fname, packer in packers.items():
if force_value:
self.add_line(f"value = self.{fname}")
alias = aliases.get(fname)
default = self.get_field_default(fname, call_factory=True)
if fname in nullable_fields:
if (
packer == "value"
and not omit_none
and not omit_none_feature
and not (omit_default and default is None)
):
self._pack_method_set_value(
fname, alias, by_alias_feature, f"self.{fname}"
fname=fname,
alias=alias,
by_alias_feature=by_alias_feature,
packed_value=(
"value" if force_value else f"self.{fname}"
),
omit_default=omit_default,
)
continue
self.add_line(f"value = self.{fname}")
if not force_value: # to add it only once
self.add_line(f"value = self.{fname}")
with self.indent("if value is not None:"):
self._pack_method_set_value(
fname, alias, by_alias_feature, packer
fname=fname,
alias=alias,
by_alias_feature=by_alias_feature,
packed_value=packer,
omit_default=(
omit_default and default is not None
),
)
if omit_none and not omit_none_feature:
continue
elif omit_default and default is None:
continue
with self.indent("else:"):
if omit_none_feature:
with self.indent("if not omit_none:"):
self._pack_method_set_value(
fname, alias, by_alias_feature, "None"
fname=fname,
alias=alias,
by_alias_feature=by_alias_feature,
packed_value="None",
omit_default=False,
)
else:
self._pack_method_set_value(
fname, alias, by_alias_feature, "None"
fname=fname,
alias=alias,
by_alias_feature=by_alias_feature,
packed_value="None",
omit_default=False,
)
else:
self._pack_method_set_value(
fname, alias, by_alias_feature, packer
fname=fname,
alias=alias,
by_alias_feature=by_alias_feature,
packed_value=packer,
omit_default=omit_default,
)
else:
kwargs_parts = []
Expand Down Expand Up @@ -962,6 +1003,29 @@ def _pack_method_set_value(
alias: typing.Optional[str],
by_alias_feature: bool,
packed_value: str,
omit_default: bool,
) -> None:
if omit_default:
default = self.get_field_default(fname, call_factory=True)
if default is not MISSING:
default_literal = self._get_field_default_literal(
self.get_field_default(fname, call_factory=True)
)
comp_op = "is not" if default_literal == "None" else "!="
with self.indent(f"if value {comp_op} {default_literal}:"):
return self.__pack_method_set_value(
fname, alias, by_alias_feature, packed_value
)
return self.__pack_method_set_value(
fname, alias, by_alias_feature, packed_value
)

def __pack_method_set_value(
self,
fname: str,
alias: typing.Optional[str],
by_alias_feature: bool,
packed_value: str,
) -> None:
if by_alias_feature and alias is not None:
with self.indent("if by_alias:"):
Expand Down Expand Up @@ -1067,6 +1131,7 @@ def _get_field_packer(
fname: str,
ftype: typing.Type,
config: typing.Type[BaseConfig],
force_value: bool = False,
) -> typing.Tuple[str, typing.Optional[str], bool]:
metadata = self.metadatas.get(fname, {})
alias = metadata.get("alias")
Expand All @@ -1078,7 +1143,7 @@ def _get_field_packer(
or is_optional(ftype, self.get_field_resolved_type_params(fname))
or self.get_field_default(fname) is None
)
value = "value" if could_be_none else f"self.{fname}"
value = "value" if could_be_none or force_value else f"self.{fname}"
packer = PackerRegistry.get(
ValueSpec(
type=ftype,
Expand Down Expand Up @@ -1135,3 +1200,15 @@ def _get_dialect_or_config_option(
if value is not Sentinel.MISSING:
return value
return default

def _get_field_default_literal(self, value: typing.Any) -> str:
if isinstance(
value, (str, int, float, bool, NoneType) # type: ignore
):
return repr(value)
elif isinstance(value, tuple) and not is_named_tuple(type(value)):
return repr(value)
else:
name = f"v_{uuid.uuid4().hex}"
self.ensure_object_imported(value, name)
return name
1 change: 1 addition & 0 deletions mashumaro/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
class Dialect:
serialization_strategy: Dict[Any, SerializationStrategyValueType] = {}
omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
omit_default: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
Loading

0 comments on commit 75dbebc

Please sign in to comment.