Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add omit_default config and dialect option #162

Merged
merged 5 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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