Skip to content

Commit

Permalink
feat: Switch toml library to tomli
Browse files Browse the repository at this point in the history
A few differences
* `None` can not be (de)serialized
* `Set` can not be (de)serialized
  • Loading branch information
yukinarit committed Jul 7, 2022
1 parent 2633b73 commit 11b4df6
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 74 deletions.
3 changes: 1 addition & 2 deletions examples/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ authors = ["yukinarit <yukinarit84@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.7.0"
pyserde = {path = "../"}
toml = "*"
pyserde = {path = "../", extras = ["all"]}
requests = "*"
envclasses = "^0.2.1"
10 changes: 6 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ typing_extensions = { version = "*", markers = "python_version ~= '3.7.0'" }
casefy = "*"
jinja2 = "*"
msgpack = { version = "*", markers = "extra == 'msgpack' or extra == 'all'", optional = true }
toml = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
tomli = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
tomli-w = { version = "*", markers = "extra == 'toml' or extra == 'all'", optional = true }
pyyaml = { version = "*", markers = "extra == 'yaml' or extra == 'all'", optional = true }
numpy = [
{ version = "~1.21.0,<1.23.0", markers = "python_version ~= '3.7.0' and (extra == 'numpy' or extra == 'all')", optional = true },
Expand All @@ -41,7 +42,8 @@ orjson = { version = "*", markers = "extra == 'orjson' or extra == 'all'", optio

[tool.poetry.dev-dependencies]
pyyaml = "*"
toml = "*"
tomli = "*"
tomli-w = "*"
msgpack = "*"
numpy = [
{ version = "~1.21.0", markers = "python_version ~= '3.7.0'" },
Expand All @@ -65,10 +67,10 @@ types-PyYAML = "^6.0.9"
[tool.poetry.extras]
msgpack = ["msgpack"]
numpy = ["numpy"]
toml = ["toml"]
toml = ["tomli", "tomli-w"]
yaml = ["pyyaml"]
orjson = ["orjson"]
all = ["msgpack", "toml", "pyyaml", "numpy", "orjson"]
all = ["msgpack", "tomli", "tomli-w", "pyyaml", "numpy", "orjson"]

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
11 changes: 6 additions & 5 deletions serde/toml.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""
Serialize and Deserialize in TOML format. This module depends on [toml](https://pypi.org/project/toml/) package.
Serialize and Deserialize in TOML format. This module depends on [tomli](https://github.com/hukkin/tomli) and [tomli-w](https://github.com/hukkin/tomli-w) packages.
"""
from typing import Type

import toml
import tomli
import tomli_w

from .compat import T
from .core import Coerce, TypeCheck
Expand All @@ -16,21 +17,21 @@
class TomlSerializer(Serializer):
@classmethod
def serialize(cls, obj, **opts) -> str:
return toml.dumps(obj, **opts)
return tomli_w.dumps(obj, **opts)


class TomlDeserializer(Deserializer):
@classmethod
def deserialize(cls, s, **opts):
return toml.loads(s, **opts)
return tomli.loads(s, **opts)


def to_toml(obj, se: Type[Serializer] = TomlSerializer, type_check: TypeCheck = Coerce, **opts) -> str:
"""
Serialize the object into TOML.
You can pass any serializable `obj`. If you supply keyword arguments other than `se`,
they will be passed in `toml.dumps` function.
they will be passed in `toml_w.dumps` function.
If you want to use the other toml package, you can subclass `TomlSerializer` and implement your own logic.
"""
Expand Down
111 changes: 63 additions & 48 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pathlib
import sys
import uuid
from typing import Any, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar
from typing import Any, Callable, Dict, Generic, List, NewType, Optional, Set, Tuple, TypeVar

import more_itertools

Expand Down Expand Up @@ -42,60 +42,75 @@ class GenericClass(Generic[T, U]):
b: U


def param(val, typ, filter: Optional[Callable] = None):
"""
Create a test parameter
* `val` is the expected value
* `typ` is the expected type
* If `filter` evaluates to True, it will filter this test case.
"""
return (val, typ, filter or (lambda se, de, opt: False))


def toml_not_supported(se, de, opt) -> bool:
return se is to_toml


types: List = [
(10, int), # Primitive
('foo', str),
(100.0, float),
(True, bool),
(10, Optional[int]), # Optional
(None, Optional[int]),
([1, 2], List[int]), # Container
([1, 2], List),
([1, 2], list),
([], List[int]),
({1, 2}, Set[int]),
({1, 2}, Set),
({1, 2}, set),
(set(), Set[int]),
((1, 1), Tuple[int, int]),
((1, 1), Tuple),
({'a': 1}, Dict[str, int]),
({'a': 1}, Dict),
({'a': 1}, dict),
({}, Dict[str, int]),
(data.Pri(10, 'foo', 100.0, True), data.Pri), # dataclass
(data.Pri(10, 'foo', 100.0, True), Optional[data.Pri]),
(None, Optional[data.Pri]),
(10, NewType('Int', int)), # NewType
({'a': 1}, Any), # Any
(GenericClass[str, int]('foo', 10), GenericClass[str, int]), # Generic
(pathlib.Path('/tmp/foo'), pathlib.Path), # Extended types
(pathlib.Path('/tmp/foo'), Optional[pathlib.Path]),
(None, Optional[pathlib.Path]),
(pathlib.PurePath('/tmp/foo'), pathlib.PurePath),
(pathlib.PurePosixPath('/tmp/foo'), pathlib.PurePosixPath),
(pathlib.PureWindowsPath('C:\\tmp'), pathlib.PureWindowsPath),
(uuid.UUID("8f85b32c-a0be-466c-87eb-b7bbf7a01683"), uuid.UUID),
(ipaddress.IPv4Address("127.0.0.1"), ipaddress.IPv4Address),
(ipaddress.IPv6Address("::1"), ipaddress.IPv6Address),
(ipaddress.IPv4Network("127.0.0.0/8"), ipaddress.IPv4Network),
(ipaddress.IPv6Network("::/128"), ipaddress.IPv6Network),
(ipaddress.IPv4Interface("192.168.1.1/24"), ipaddress.IPv4Interface),
(ipaddress.IPv6Interface("::1/128"), ipaddress.IPv6Interface),
(decimal.Decimal(10), decimal.Decimal),
(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p'), datetime.datetime),
(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').date(), datetime.date),
(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').time(), datetime.time),
param(10, int), # Primitive
param('foo', str),
param(100.0, float),
param(True, bool),
param(10, Optional[int]), # Optional
param(None, Optional[int], toml_not_supported),
param([1, 2], List[int]), # Container
param([1, 2], List),
param([1, 2], list),
param([], List[int]),
param({1, 2}, Set[int], toml_not_supported),
param({1, 2}, Set, toml_not_supported),
param({1, 2}, set, toml_not_supported),
param(set(), Set[int], toml_not_supported),
param((1, 1), Tuple[int, int]),
param((1, 1), Tuple),
param({'a': 1}, Dict[str, int]),
param({'a': 1}, Dict),
param({'a': 1}, dict),
param({}, Dict[str, int]),
param(data.Pri(10, 'foo', 100.0, True), data.Pri), # dataclass
param(data.Pri(10, 'foo', 100.0, True), Optional[data.Pri]),
param(None, Optional[data.Pri], toml_not_supported),
param(10, NewType('Int', int)), # NewType
param({'a': 1}, Any), # Any
param(GenericClass[str, int]('foo', 10), GenericClass[str, int]), # Generic
param(pathlib.Path('/tmp/foo'), pathlib.Path), # Extended types
param(pathlib.Path('/tmp/foo'), Optional[pathlib.Path]),
param(None, Optional[pathlib.Path], toml_not_supported),
param(pathlib.PurePath('/tmp/foo'), pathlib.PurePath),
param(pathlib.PurePosixPath('/tmp/foo'), pathlib.PurePosixPath),
param(pathlib.PureWindowsPath('C:\\tmp'), pathlib.PureWindowsPath),
param(uuid.UUID("8f85b32c-a0be-466c-87eb-b7bbf7a01683"), uuid.UUID),
param(ipaddress.IPv4Address("127.0.0.1"), ipaddress.IPv4Address),
param(ipaddress.IPv6Address("::1"), ipaddress.IPv6Address),
param(ipaddress.IPv4Network("127.0.0.0/8"), ipaddress.IPv4Network),
param(ipaddress.IPv6Network("::/128"), ipaddress.IPv6Network),
param(ipaddress.IPv4Interface("192.168.1.1/24"), ipaddress.IPv4Interface),
param(ipaddress.IPv6Interface("::1/128"), ipaddress.IPv6Interface),
param(decimal.Decimal(10), decimal.Decimal),
param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p'), datetime.datetime),
param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').date(), datetime.date),
param(datetime.datetime.strptime('Jan 1 2021 1:55PM', '%b %d %Y %I:%M%p').time(), datetime.time),
]

# these types can only be instantiated on their corresponding system
if os.name == "posix":
types.append((pathlib.PosixPath('/tmp/foo'), pathlib.PosixPath))
types.append(param(pathlib.PosixPath('/tmp/foo'), pathlib.PosixPath))
if os.name == "nt":
types.append((pathlib.WindowsPath('C:\\tmp'), pathlib.WindowsPath))
types.append(param(pathlib.WindowsPath('C:\\tmp'), pathlib.WindowsPath))

if sys.version_info[:3] >= (3, 9, 0):
types.extend([([1, 2], list[int]), ({'a': 1}, dict[str, int]), ((1, 1), tuple[int, int])])
types.extend([param([1, 2], list[int]), param({'a': 1}, dict[str, int]), param((1, 1), tuple[int, int])])

types_combinations: List = list(map(lambda c: list(more_itertools.flatten(c)), itertools.combinations(types, 2)))

Expand All @@ -122,7 +137,7 @@ def type_ids():
from serde.compat import typename

def make_id(pair: Tuple):
t, T = pair
t, T, _ = pair
return f'{typename(T)}({t})'

return list(map(make_id, types))
Expand Down
60 changes: 45 additions & 15 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
serde.init(True)


@pytest.mark.parametrize('t,T', types, ids=type_ids())
@pytest.mark.parametrize('t,T,f', types, ids=type_ids())
@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
@pytest.mark.parametrize('se,de', all_formats)
def test_simple(se, de, opt, t, T):
def test_simple(se, de, opt, t, T, f):
log.info(f'Running test with se={se.__name__} de={de.__name__} opts={opt}')

if f(se, de, opt):
return

@serde.serde(**opt)
class C:
i: int
Expand All @@ -63,10 +66,10 @@ class C:
assert t == de(T, se(t, type_check=Strict), type_check=Strict)


@pytest.mark.parametrize('t,T', types, ids=type_ids())
@pytest.mark.parametrize('t,T,filter', types, ids=type_ids())
@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
@pytest.mark.parametrize('se,de', (format_dict + format_tuple))
def test_simple_with_reuse_instances(se, de, opt, t, T):
def test_simple_with_reuse_instances(se, de, opt, t, T, filter):
log.info(f'Running test with se={se.__name__} de={de.__name__} opts={opt} while reusing instances')

@serde.serde(**opt)
Expand Down Expand Up @@ -334,18 +337,19 @@ def test_default(se, de):
assert p == from_dict(PriDefault, {'i': 10, 's': 'foo', 'f': 100.0, 'b': True})
assert p == from_tuple(PriDefault, (10, 'foo', 100.0, True))

o = OptDefault()
assert o == de(OptDefault, se(o))

o = OptDefault()
assert o == from_dict(OptDefault, {})
assert o == from_dict(OptDefault, {"n": None})
assert o == from_dict(OptDefault, {"n": None, "i": 10})
assert o == from_tuple(OptDefault, (None, 10))
if se is not serde.toml.to_toml:
o = OptDefault()
assert o == de(OptDefault, se(o))
o = OptDefault()
assert o == from_dict(OptDefault, {})
assert o == from_dict(OptDefault, {"n": None})
assert o == from_dict(OptDefault, {"n": None, "i": 10})
assert o == from_tuple(OptDefault, (None, 10))

o = OptDefault(n=None, i=None)
assert o == from_dict(OptDefault, {"n": None, "i": None})
assert o == from_tuple(OptDefault, (None, None))
if se is not serde.toml.to_toml:
o = OptDefault(n=None, i=None)
assert o == from_dict(OptDefault, {"n": None, "i": None})
assert o == from_tuple(OptDefault, (None, None))

assert 10 == dataclasses.fields(PriDefault)[0].default
assert 'foo' == dataclasses.fields(PriDefault)[1].default
Expand Down Expand Up @@ -396,6 +400,32 @@ def test_msgpack_unnamed():
assert p == serde.msgpack.from_msgpack(data.Pri, d, named=False)


def test_toml():
@serde.serde
@dataclasses.dataclass
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
@dataclasses.dataclass
class Foo:
v: Set[int]

# TODO: Should raise SerdeError
with pytest.raises(TypeError):
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

0 comments on commit 11b4df6

Please sign in to comment.