Skip to content

Commit f93cafa

Browse files
authored
Merge pull request #601 from yukinarit/add-skip-none
Add skip_none parameter
2 parents ed0fc55 + 67f98de commit f93cafa

File tree

9 files changed

+121
-34
lines changed

9 files changed

+121
-34
lines changed

examples/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,5 +132,7 @@ def run(module: typing.Any) -> None:
132132
if __name__ == "__main__":
133133
try:
134134
run_all()
135+
print("-----------------")
136+
print("all tests passed successfully!")
135137
except Exception:
136138
sys.exit(1)

serde/json.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def to_json(
5656
se: type[Serializer[str]] = JsonSerializer,
5757
reuse_instances: bool = False,
5858
convert_sets: bool = True,
59+
skip_none: bool = False,
5960
**opts: Any,
6061
) -> str:
6162
"""
@@ -68,6 +69,9 @@ def to_json(
6869
argument with [orjson](https://github.com/ijl/orjson#numpy), or the `default` argument with
6970
Python standard json library.
7071
72+
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
73+
serialized output. Defaults to False.
74+
7175
If you want to use another json package, you can subclass `JsonSerializer` and implement
7276
your own logic.
7377
"""

serde/msgpack.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,12 @@ def to_msgpack(
5656
as a `msgpack.ExtType` If you supply other keyword arguments, they will be passed in
5757
`msgpack.packb` function.
5858
59-
If `named` is True, field names are preserved, namely the object is encoded as `dict` then
60-
serialized into MsgPack. If `named` is False, the object is encoded as `tuple` then serialized
61-
into MsgPack. `named=False` will produces compact binary.
59+
* `named`: If `named` is True, field names are preserved, namely the object is encoded as `dict`
60+
then serialized into MsgPack. If `named` is False, the object is encoded as `tuple` then
61+
serialized into MsgPack. `named=False` will produces compact binary.
62+
63+
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
64+
serialized output. Defaults to False.
6265
6366
If you want to use the other msgpack package, you can subclass `MsgPackSerializer` and
6467
implement your own logic.
@@ -107,6 +110,7 @@ def from_msgpack(
107110
de: type[Deserializer[bytes]] = MsgPackDeserializer,
108111
named: bool = True,
109112
ext_dict: Optional[dict[int, type[Any]]] = None,
113+
skip_none: bool = False,
110114
**opts: Any,
111115
) -> Any:
112116
"""

serde/pickle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def to_pickle(
3030
se: type[Serializer[bytes]] = PickleSerializer,
3131
reuse_instances: bool = False,
3232
convert_sets: bool = True,
33+
skip_none: bool = False,
3334
**opts: Any,
3435
) -> bytes:
3536
return se.serialize(

serde/se.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -334,13 +334,17 @@ def to_obj(
334334
named: bool,
335335
reuse_instances: Optional[bool] = None,
336336
convert_sets: Optional[bool] = None,
337+
skip_none: bool = False,
337338
c: Optional[Any] = None,
338339
) -> Any:
339340
def serializable_to_obj(object: Any) -> Any:
340341
serde_scope: Scope = getattr(object, SERDE_SCOPE)
341342
func_name = TO_DICT if named else TO_ITER
342343
return serde_scope.funcs[func_name](
343-
object, reuse_instances=reuse_instances, convert_sets=convert_sets
344+
object,
345+
reuse_instances=reuse_instances,
346+
convert_sets=convert_sets,
347+
skip_none=skip_none,
344348
)
345349

346350
try:
@@ -349,6 +353,7 @@ def serializable_to_obj(object: Any) -> Any:
349353
named=named,
350354
reuse_instances=reuse_instances,
351355
convert_sets=convert_sets,
356+
skip_none=skip_none,
352357
)
353358

354359
# If a class in the argument is a non-dataclass class e.g. Union[Foo, Bar],
@@ -377,7 +382,11 @@ def serializable_to_obj(object: Any) -> Any:
377382
return {k: thisfunc(v) for k, v in o.items()}
378383
elif is_str_serializable_instance(o) or is_datetime_instance(o):
379384
return CACHE.serialize(
380-
c or o.__class__, o, reuse_instances=reuse_instances, convert_sets=convert_sets
385+
c or o.__class__,
386+
o,
387+
reuse_instances=reuse_instances,
388+
convert_sets=convert_sets,
389+
skip_none=skip_none,
381390
)
382391

383392
return o
@@ -398,6 +407,7 @@ def to_tuple(
398407
c: Optional[type[Any]] = None,
399408
reuse_instances: Optional[bool] = None,
400409
convert_sets: Optional[bool] = None,
410+
skip_none: bool = False,
401411
) -> tuple[Any, ...]:
402412
"""
403413
Serialize object into tuple.
@@ -419,7 +429,12 @@ def to_tuple(
419429
[(10, 'foo', 100.0, True), (20, 'foo', 100.0, True)]
420430
"""
421431
return to_obj( # type: ignore
422-
o, named=False, c=c, reuse_instances=reuse_instances, convert_sets=convert_sets
432+
o,
433+
named=False,
434+
c=c,
435+
reuse_instances=reuse_instances,
436+
convert_sets=convert_sets,
437+
skip_none=skip_none,
423438
)
424439

425440

@@ -435,6 +450,7 @@ def to_dict(
435450
c: Optional[type[Any]] = None,
436451
reuse_instances: Optional[bool] = None,
437452
convert_sets: Optional[bool] = None,
453+
skip_none: bool = False,
438454
) -> dict[Any, Any]:
439455
"""
440456
Serialize object into python dictionary. This function ensures that the dataclass's fields are
@@ -450,6 +466,8 @@ def to_dict(
450466
deserialization. When `convert_sets` is set to True, pyserde will convert sets to lists during
451467
serialization and back to sets during deserialization. This is useful for data formats that
452468
do not natively support sets.
469+
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
470+
serialized output. Defaults to False.
453471
454472
>>> from serde import serde
455473
>>> @serde
@@ -470,7 +488,12 @@ def to_dict(
470488
[{'i': 10, 's': 'foo', 'f': 100.0, 'b': True}, {'i': 20, 's': 'foo', 'f': 100.0, 'b': True}]
471489
"""
472490
return to_obj( # type: ignore
473-
o, named=True, c=c, reuse_instances=reuse_instances, convert_sets=convert_sets
491+
o,
492+
named=True,
493+
c=c,
494+
reuse_instances=reuse_instances,
495+
convert_sets=convert_sets,
496+
skip_none=skip_none,
474497
)
475498

476499

@@ -524,7 +547,7 @@ def sefields(cls: type[Any], serialize_class_var: bool = False) -> Iterator[SeFi
524547
loader=jinja2.DictLoader(
525548
{
526549
"dict": """
527-
def {{func}}(obj, reuse_instances = None, convert_sets = None):
550+
def {{func}}(obj, reuse_instances = None, convert_sets = None, skip_none = False):
528551
if reuse_instances is None:
529552
reuse_instances = {{serde_scope.reuse_instances_default}}
530553
if convert_sets is None:
@@ -534,21 +557,25 @@ def {{func}}(obj, reuse_instances = None, convert_sets = None):
534557
535558
res = {}
536559
{% for f in fields -%}
560+
subres = {{rvalue(f)}}
537561
{% if not f.skip -%}
538562
{% if f.skip_if -%}
539-
subres = {{rvalue(f)}}
540563
if not {{f.skip_if.name}}(subres):
541564
{{lvalue(f)}} = subres
542565
{% else -%}
543-
{{lvalue(f)}} = {{rvalue(f)}}
566+
if skip_none:
567+
if subres is not None:
568+
{{lvalue(f)}} = subres
569+
else:
570+
{{lvalue(f)}} = subres
544571
{% endif -%}
545572
{% endif %}
546573
547574
{% endfor -%}
548575
return res
549576
""",
550577
"iter": """
551-
def {{func}}(obj, reuse_instances=None, convert_sets=None):
578+
def {{func}}(obj, reuse_instances=None, convert_sets=None, skip_none=False):
552579
if reuse_instances is None:
553580
reuse_instances = {{serde_scope.reuse_instances_default}}
554581
if convert_sets is None:

serde/toml.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def to_toml(
4040
se: type[Serializer[str]] = TomlSerializer,
4141
reuse_instances: bool = False,
4242
convert_sets: bool = True,
43+
skip_none: bool = True,
4344
**opts: Any,
4445
) -> str:
4546
"""
@@ -48,11 +49,21 @@ def to_toml(
4849
You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
4950
they will be passed in `toml_w.dumps` function.
5051
52+
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
53+
serialized output. Defaults to True.
54+
5155
If you want to use the other toml package, you can subclass `TomlSerializer` and implement
5256
your own logic.
5357
"""
5458
return se.serialize(
55-
to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts
59+
to_dict(
60+
obj,
61+
c=cls,
62+
reuse_instances=reuse_instances,
63+
convert_sets=convert_sets,
64+
skip_none=skip_none,
65+
),
66+
**opts,
5667
)
5768

5869

serde/yaml.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def to_yaml(
3232
se: type[Serializer[str]] = YamlSerializer,
3333
reuse_instances: bool = False,
3434
convert_sets: bool = True,
35+
skip_none: bool = False,
3536
**opts: Any,
3637
) -> str:
3738
"""
@@ -40,6 +41,9 @@ def to_yaml(
4041
You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
4142
they will be passed in `yaml.safe_dump` function.
4243
44+
* `skip_none`: When set to True, any field in the class with a None value is excluded from the
45+
serialized output. Defaults to False.
46+
4347
If you want to use the other yaml package, you can subclass `YamlSerializer` and implement
4448
your own logic.
4549
"""

tests/test_basics.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -458,28 +458,6 @@ def test_msgpack_unnamed():
458458
assert p == serde.msgpack.from_msgpack(data.Pri, d, named=False)
459459

460460

461-
def test_toml():
462-
@serde.serde
463-
class Foo:
464-
v: Optional[int]
465-
466-
f = Foo(10)
467-
assert "v = 10\n" == serde.toml.to_toml(f)
468-
assert f == serde.toml.from_toml(Foo, "v = 10\n")
469-
470-
# TODO: Should raise SerdeError
471-
with pytest.raises(TypeError):
472-
f = Foo(None)
473-
serde.toml.to_toml(f)
474-
475-
@serde.serde
476-
class Foo:
477-
v: set[int]
478-
479-
f = Foo({1, 2, 3})
480-
serde.toml.to_toml(f)
481-
482-
483461
@pytest.mark.parametrize("se,de", all_formats)
484462
def test_rename(se, de):
485463
@serde.serde

tests/test_toml.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from serde import serde
2+
from serde.toml import to_toml, from_toml
3+
from typing import Optional
4+
import pytest
5+
6+
7+
def toml_basics() -> None:
8+
@serde
9+
class Foo:
10+
v: Optional[int]
11+
12+
f = Foo(10)
13+
assert "v = 10\n" == to_toml(f)
14+
assert f == from_toml(Foo, "v = 10\n")
15+
16+
@serde
17+
class Bar:
18+
v: set[int]
19+
20+
b = Bar({1, 2, 3})
21+
to_toml(b)
22+
23+
24+
def test_skip_none() -> None:
25+
@serde
26+
class Foo:
27+
a: int
28+
b: Optional[int]
29+
30+
f = Foo(10, 100)
31+
assert (
32+
to_toml(f)
33+
== """\
34+
a = 10
35+
b = 100
36+
"""
37+
)
38+
39+
f = Foo(10, None)
40+
assert (
41+
to_toml(f)
42+
== """\
43+
a = 10
44+
"""
45+
)
46+
47+
48+
def test_skip_none_container_not_supported_yet() -> None:
49+
@serde
50+
class Foo:
51+
a: int
52+
b: list[Optional[int]]
53+
54+
f = Foo(10, [100, None])
55+
with pytest.raises(TypeError):
56+
to_toml(f)

0 commit comments

Comments
 (0)