Skip to content

Commit

Permalink
Add skip_none parameter
Browse files Browse the repository at this point in the history
The `skip_none` attribute is an optional feature originally
implemented to prevent null values from appearing in TOML
serialized outputs. When set to True, any field in the class with
a None value is excluded from the serialized output, ensuring that
TOML files (or other formats) remain clean and free from null
entries.

Closes #421
  • Loading branch information
yukinarit committed Oct 21, 2024
1 parent ed0fc55 commit 67f98de
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 34 deletions.
2 changes: 2 additions & 0 deletions examples/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,7 @@ def run(module: typing.Any) -> None:
if __name__ == "__main__":
try:
run_all()
print("-----------------")
print("all tests passed successfully!")
except Exception:
sys.exit(1)
4 changes: 4 additions & 0 deletions serde/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def to_json(
se: type[Serializer[str]] = JsonSerializer,
reuse_instances: bool = False,
convert_sets: bool = True,
skip_none: bool = False,
**opts: Any,
) -> str:
"""
Expand All @@ -68,6 +69,9 @@ def to_json(
argument with [orjson](https://github.com/ijl/orjson#numpy), or the `default` argument with
Python standard json library.
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
serialized output. Defaults to False.
If you want to use another json package, you can subclass `JsonSerializer` and implement
your own logic.
"""
Expand Down
10 changes: 7 additions & 3 deletions serde/msgpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,12 @@ def to_msgpack(
as a `msgpack.ExtType` If you supply other keyword arguments, they will be passed in
`msgpack.packb` function.
If `named` is True, field names are preserved, namely the object is encoded as `dict` then
serialized into MsgPack. If `named` is False, the object is encoded as `tuple` then serialized
into MsgPack. `named=False` will produces compact binary.
* `named`: If `named` is True, field names are preserved, namely the object is encoded as `dict`
then serialized into MsgPack. If `named` is False, the object is encoded as `tuple` then
serialized into MsgPack. `named=False` will produces compact binary.
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
serialized output. Defaults to False.
If you want to use the other msgpack package, you can subclass `MsgPackSerializer` and
implement your own logic.
Expand Down Expand Up @@ -107,6 +110,7 @@ def from_msgpack(
de: type[Deserializer[bytes]] = MsgPackDeserializer,
named: bool = True,
ext_dict: Optional[dict[int, type[Any]]] = None,
skip_none: bool = False,
**opts: Any,
) -> Any:
"""
Expand Down
1 change: 1 addition & 0 deletions serde/pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def to_pickle(
se: type[Serializer[bytes]] = PickleSerializer,
reuse_instances: bool = False,
convert_sets: bool = True,
skip_none: bool = False,
**opts: Any,
) -> bytes:
return se.serialize(
Expand Down
43 changes: 35 additions & 8 deletions serde/se.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,17 @@ def to_obj(
named: bool,
reuse_instances: Optional[bool] = None,
convert_sets: Optional[bool] = None,
skip_none: bool = False,
c: Optional[Any] = None,
) -> Any:
def serializable_to_obj(object: Any) -> Any:
serde_scope: Scope = getattr(object, SERDE_SCOPE)
func_name = TO_DICT if named else TO_ITER
return serde_scope.funcs[func_name](
object, reuse_instances=reuse_instances, convert_sets=convert_sets
object,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
)

try:
Expand All @@ -349,6 +353,7 @@ def serializable_to_obj(object: Any) -> Any:
named=named,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
)

# If a class in the argument is a non-dataclass class e.g. Union[Foo, Bar],
Expand Down Expand Up @@ -377,7 +382,11 @@ def serializable_to_obj(object: Any) -> Any:
return {k: thisfunc(v) for k, v in o.items()}
elif is_str_serializable_instance(o) or is_datetime_instance(o):
return CACHE.serialize(
c or o.__class__, o, reuse_instances=reuse_instances, convert_sets=convert_sets
c or o.__class__,
o,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
)

return o
Expand All @@ -398,6 +407,7 @@ def to_tuple(
c: Optional[type[Any]] = None,
reuse_instances: Optional[bool] = None,
convert_sets: Optional[bool] = None,
skip_none: bool = False,
) -> tuple[Any, ...]:
"""
Serialize object into tuple.
Expand All @@ -419,7 +429,12 @@ def to_tuple(
[(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]
"""
return to_obj( # type: ignore
o, named=False, c=c, reuse_instances=reuse_instances, convert_sets=convert_sets
o,
named=False,
c=c,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
)


Expand All @@ -435,6 +450,7 @@ def to_dict(
c: Optional[type[Any]] = None,
reuse_instances: Optional[bool] = None,
convert_sets: Optional[bool] = None,
skip_none: bool = False,
) -> dict[Any, Any]:
"""
Serialize object into python dictionary. This function ensures that the dataclass's fields are
Expand All @@ -450,6 +466,8 @@ def to_dict(
deserialization. When `convert_sets` is set to True, pyserde will convert sets to lists during
serialization and back to sets during deserialization. This is useful for data formats that
do not natively support sets.
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
serialized output. Defaults to False.
>>> from serde import serde
>>> @serde
Expand All @@ -470,7 +488,12 @@ def to_dict(
[{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
"""
return to_obj( # type: ignore
o, named=True, c=c, reuse_instances=reuse_instances, convert_sets=convert_sets
o,
named=True,
c=c,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
)


Expand Down Expand Up @@ -524,7 +547,7 @@ def sefields(cls: type[Any], serialize_class_var: bool = False) -> Iterator[SeFi
loader=jinja2.DictLoader(
{
"dict": """
def {{func}}(obj, reuse_instances = None, convert_sets = None):
def {{func}}(obj, reuse_instances = None, convert_sets = None, skip_none = False):
if reuse_instances is None:
reuse_instances = {{serde_scope.reuse_instances_default}}
if convert_sets is None:
Expand All @@ -534,21 +557,25 @@ def {{func}}(obj, reuse_instances = None, convert_sets = None):
res = {}
{% for f in fields -%}
subres = {{rvalue(f)}}
{% if not f.skip -%}
{% if f.skip_if -%}
subres = {{rvalue(f)}}
if not {{f.skip_if.name}}(subres):
{{lvalue(f)}} = subres
{% else -%}
{{lvalue(f)}} = {{rvalue(f)}}
if skip_none:
if subres is not None:
{{lvalue(f)}} = subres
else:
{{lvalue(f)}} = subres
{% endif -%}
{% endif %}
{% endfor -%}
return res
""",
"iter": """
def {{func}}(obj, reuse_instances=None, convert_sets=None):
def {{func}}(obj, reuse_instances=None, convert_sets=None, skip_none=False):
if reuse_instances is None:
reuse_instances = {{serde_scope.reuse_instances_default}}
if convert_sets is None:
Expand Down
13 changes: 12 additions & 1 deletion serde/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def to_toml(
se: type[Serializer[str]] = TomlSerializer,
reuse_instances: bool = False,
convert_sets: bool = True,
skip_none: bool = True,
**opts: Any,
) -> str:
"""
Expand All @@ -48,11 +49,21 @@ def to_toml(
You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
they will be passed in `toml_w.dumps` function.
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
serialized output. Defaults to True.
If you want to use the other toml package, you can subclass `TomlSerializer` and implement
your own logic.
"""
return se.serialize(
to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
to_dict(
obj,
c=cls,
reuse_instances=reuse_instances,
convert_sets=convert_sets,
skip_none=skip_none,
),
**opts,
)


Expand Down
4 changes: 4 additions & 0 deletions serde/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def to_yaml(
se: type[Serializer[str]] = YamlSerializer,
reuse_instances: bool = False,
convert_sets: bool = True,
skip_none: bool = False,
**opts: Any,
) -> str:
"""
Expand All @@ -40,6 +41,9 @@ def to_yaml(
You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
they will be passed in `yaml.safe_dump` function.
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
serialized output. Defaults to False.
If you want to use the other yaml package, you can subclass `YamlSerializer` and implement
your own logic.
"""
Expand Down
22 changes: 0 additions & 22 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,28 +458,6 @@ def test_msgpack_unnamed():
assert p == serde.msgpack.from_msgpack(data.Pri, d, named=False)


def test_toml():
@serde.serde
class Foo:
v: Optional[int]

f = Foo(10)
assert "v = 10\n" == serde.toml.to_toml(f)
assert f == serde.toml.from_toml(Foo, "v = 10\n")

# TODO: Should raise SerdeError
with pytest.raises(TypeError):
f = Foo(None)
serde.toml.to_toml(f)

@serde.serde
class Foo:
v: set[int]

f = Foo({1, 2, 3})
serde.toml.to_toml(f)


@pytest.mark.parametrize("se,de", all_formats)
def test_rename(se, de):
@serde.serde
Expand Down
56 changes: 56 additions & 0 deletions tests/test_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from serde import serde
from serde.toml import to_toml, from_toml
from typing import Optional
import pytest


def toml_basics() -> None:
@serde
class Foo:
v: Optional[int]

f = Foo(10)
assert "v = 10\n" == to_toml(f)
assert f == from_toml(Foo, "v = 10\n")

@serde
class Bar:
v: set[int]

b = Bar({1, 2, 3})
to_toml(b)


def test_skip_none() -> None:
@serde
class Foo:
a: int
b: Optional[int]

f = Foo(10, 100)
assert (
to_toml(f)
== """\
a = 10
b = 100
"""
)

f = Foo(10, None)
assert (
to_toml(f)
== """\
a = 10
"""
)


def test_skip_none_container_not_supported_yet() -> None:
@serde
class Foo:
a: int
b: list[Optional[int]]

f = Foo(10, [100, None])
with pytest.raises(TypeError):
to_toml(f)

0 comments on commit 67f98de

Please sign in to comment.