Skip to content

Commit 0a597e5

Browse files
authored
Merge pull request #321 from yukinarit/fix-variable-length-tuple
Fix variable length of tuple
2 parents 322805f + 6a3324a commit 0a597e5

9 files changed

+96
-23
lines changed

examples/runner.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import custom_field_serializer
99
import default
1010
import default_dict
11-
import ellipsis
1211
import env
1312
import flatten
1413
import forward_reference
@@ -36,6 +35,7 @@
3635
import union
3736
import union_tagging
3837
import user_exception
38+
import variable_length_tuple
3939
import yamlfile
4040

4141
PY310 = sys.version_info[:3] >= (3, 10, 0)
@@ -72,7 +72,7 @@ def run_all():
7272
run(type_check_coerce)
7373
run(user_exception)
7474
run(pep681)
75-
run(ellipsis)
75+
run(variable_length_tuple)
7676
run(init_var)
7777
run(class_var)
7878
run(alias)

examples/ellipsis.py examples/variable_length_tuple.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ class Foo:
1212

1313

1414
def main():
15-
f = Foo(v=(10, 'foo'))
15+
f = Foo(v=(1, 2, 3))
1616
print(f"Into Json: {to_json(f)}")
1717

18-
s = '{"v": [10, "foo"]}'
18+
s = '{"v": [1, 2, 3]}'
1919
print(f"From Json: {from_json(Foo, s)}")
2020

2121

serde/compat.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def typename(typ: Type[Any], with_typing_module: bool = False) -> str:
241241
return f'Literal[{", ".join(str(e) for e in args)}]'
242242
elif typ is Any:
243243
return f'{mod}Any'
244-
elif typ is Ellipsis:
244+
elif is_ellipsis(typ):
245245
return '...'
246246
else:
247247
# Get super type for NewType
@@ -569,6 +569,23 @@ def is_bare_tuple(typ: Type[Any]) -> bool:
569569
return typ in (Tuple, tuple)
570570

571571

572+
def is_variable_tuple(typ: Type[Any]) -> bool:
573+
"""
574+
Test if the type is a variable length of tuple `typing.Tuple[T, ...]`.
575+
576+
>>> from typing import Tuple
577+
>>> is_variable_tuple(Tuple[int, ...])
578+
True
579+
>>> is_variable_tuple(Tuple[int, bool])
580+
False
581+
>>> is_variable_tuple(Tuple[()])
582+
False
583+
"""
584+
istuple = is_tuple(typ) and not is_bare_tuple(typ)
585+
args = get_args(typ)
586+
return istuple and len(args) == 2 and is_ellipsis(args[1])
587+
588+
572589
def is_set(typ: Type[Any]) -> bool:
573590
"""
574591
Test if the type is `typing.Set` or `typing.FrozenSet`.
@@ -791,6 +808,10 @@ def is_datetime_instance(obj: Any) -> bool:
791808
return isinstance(obj, DateTimeTypes)
792809

793810

811+
def is_ellipsis(typ: Any) -> bool:
812+
return typ is Ellipsis
813+
814+
794815
def find_generic_arg(cls: Type[Any], field: TypeVar) -> int:
795816
"""
796817
Find a type in generic parameters.

serde/core.py

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
is_set,
3434
is_tuple,
3535
is_union,
36+
is_variable_tuple,
3637
type_args,
3738
typename,
3839
)
@@ -251,6 +252,11 @@ def is_set_instance(obj: Any, typ: Type) -> bool:
251252
def is_tuple_instance(obj: Any, typ: Type) -> bool:
252253
if not isinstance(obj, tuple):
253254
return False
255+
if is_variable_tuple(typ):
256+
arg = type_args(typ)[0]
257+
for v in obj:
258+
if not is_instance(v, arg):
259+
return False
254260
if len(obj) == 0 or is_bare_tuple(typ):
255261
return True
256262
for i, arg in enumerate(type_args(typ)):

serde/de.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
is_datetime,
3434
is_default_dict,
3535
is_dict,
36+
is_ellipsis,
3637
is_enum,
3738
is_frozen_set,
3839
is_generic,
@@ -45,6 +46,7 @@
4546
is_str_serializable,
4647
is_tuple,
4748
is_union,
49+
is_variable_tuple,
4850
iter_literals,
4951
iter_types,
5052
iter_unions,
@@ -425,7 +427,7 @@ def deserializable_to_obj(cls):
425427
res = set(thisfunc(type_args(c)[0], e) for e in o)
426428
return res
427429
elif is_tuple(c):
428-
if is_bare_tuple(c):
430+
if is_bare_tuple(c) or is_variable_tuple(c):
429431
return tuple(e for e in o)
430432
else:
431433
res = tuple(thisfunc(type_args(c)[i], e) for i, e in enumerate(o))
@@ -448,7 +450,7 @@ def deserializable_to_obj(cls):
448450
return deserialize_numpy_array_direct(c, o)
449451
elif is_datetime(c):
450452
return c.fromisoformat(o)
451-
elif is_any(c) or c is Ellipsis:
453+
elif is_any(c) or is_ellipsis(c):
452454
return o
453455

454456
return c(o)
@@ -641,7 +643,7 @@ def render(self, arg: DeField) -> str:
641643
if reuse_instances else {from_iso}"
642644
elif is_none(arg.type):
643645
res = "None"
644-
elif arg.type is Any or arg.type is Ellipsis:
646+
elif is_any(arg.type) or is_ellipsis(arg.type):
645647
res = arg.data
646648
elif isinstance(arg.type, TypeVar):
647649
index = find_generic_arg(self.cls, arg.type)
@@ -786,7 +788,7 @@ def tuple(self, arg: DeField) -> str:
786788
[coerce(int, v) for v in data[0][2]], Foo.__serde__.funcs['foo'](data=data[0][3], \
787789
maybe_generic=maybe_generic, reuse_instances=reuse_instances),)"
788790
"""
789-
if is_bare_tuple(arg.type):
791+
if is_bare_tuple(arg.type) or is_variable_tuple(arg.type):
790792
return f'tuple({arg.data})'
791793
else:
792794
values = []

serde/se.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
SerdeError,
2020
SerdeSkip,
2121
get_origin,
22+
is_any,
2223
is_bare_dict,
2324
is_bare_list,
2425
is_bare_opt,
@@ -28,6 +29,7 @@
2829
is_datetime,
2930
is_datetime_instance,
3031
is_dict,
32+
is_ellipsis,
3133
is_enum,
3234
is_generic,
3335
is_list,
@@ -40,6 +42,7 @@
4042
is_str_serializable_instance,
4143
is_tuple,
4244
is_union,
45+
is_variable_tuple,
4346
iter_types,
4447
iter_unions,
4548
type_args,
@@ -719,7 +722,7 @@ def render(self, arg: SeField) -> str:
719722
res = f"{arg.varname} if reuse_instances else {arg.varname}.isoformat()"
720723
elif is_none(arg.type):
721724
res = "None"
722-
elif arg.type is Any or arg.type is Ellipsis or isinstance(arg.type, TypeVar):
725+
elif is_any(arg.type) or isinstance(arg.type, TypeVar):
723726
res = f"to_obj({arg.varname}, True, False, False)"
724727
elif is_generic(arg.type):
725728
arg.type = get_origin(arg.type)
@@ -801,7 +804,7 @@ def tuple(self, arg: SeField) -> str:
801804
"""
802805
Render rvalue for tuple.
803806
"""
804-
if is_bare_tuple(arg.type):
807+
if is_bare_tuple(arg.type) or is_variable_tuple(arg.type):
805808
return arg.varname
806809
else:
807810
rvalues = []

tests/common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def toml_not_supported(se, de, opt) -> bool:
8080
param({1, 2}, FrozenSet[int], toml_not_supported),
8181
param((1, 1), Tuple[int, int]),
8282
param((1, 1), Tuple),
83-
param((1, 1), Tuple[int, ...]),
83+
param((1, 2, 3), Tuple[int, ...]),
8484
param({'a': 1}, Dict[str, int]),
8585
param({'a': 1}, Dict),
8686
param({'a': 1}, dict),

tests/test_basics.py

+48-10
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ class C:
6464
assert t == de(T, se(t))
6565

6666

67+
@pytest.mark.parametrize('t,T,f', types, ids=type_ids())
68+
@pytest.mark.parametrize('named', (True, False))
69+
@pytest.mark.parametrize('reuse', (True, False))
70+
def test_from_to_obj(t, T, f, named, reuse):
71+
obj = serde.se.to_obj(t, named, reuse, False)
72+
assert t == serde.de.from_obj(T, obj, named, reuse)
73+
74+
6775
@pytest.mark.parametrize('t,T,filter', types, ids=type_ids())
6876
@pytest.mark.parametrize('opt', opt_case, ids=opt_case_ids())
6977
@pytest.mark.parametrize('se,de', (format_dict + format_tuple))
@@ -244,36 +252,43 @@ def test_enum_imported(se, de):
244252
@pytest.mark.parametrize('se,de', all_formats)
245253
def test_tuple(se, de, opt):
246254
@serde.serde(**opt)
255+
@dataclasses.dataclass
247256
class Homogeneous:
248257
i: Tuple[int, int]
249258
s: Tuple[str, str]
250259
f: Tuple[float, float]
251260
b: Tuple[bool, bool]
252261

253-
p = Homogeneous((10, 20), ('a', 'b'), (10.0, 20.0), (True, False))
254-
assert p == de(Homogeneous, se(p))
262+
a = Homogeneous((10, 20), ('a', 'b'), (10.0, 20.0), (True, False))
263+
assert a == de(Homogeneous, se(a))
255264

256265
# List will be type mismatch if type_check=True.
257-
p = Homogeneous([10, 20], ['a', 'b'], [10.0, 20.0], [True, False])
258-
p != de(Homogeneous, se(p))
266+
a = Homogeneous([10, 20], ['a', 'b'], [10.0, 20.0], [True, False])
267+
a != de(Homogeneous, se(a))
259268

260269
@serde.serde(**opt)
270+
@dataclasses.dataclass
261271
class Variant:
262272
t: Tuple[int, str, float, bool]
263273

264274
# Toml doesn't support variant type of array.
265275
if se is not serde.toml.to_toml:
266-
p = Variant((10, 'a', 10.0, True))
267-
assert p == de(Variant, se(p))
276+
b = Variant((10, 'a', 10.0, True))
277+
assert b == de(Variant, se(b))
268278

269279
@serde.serde(**opt)
280+
@dataclasses.dataclass
270281
class BareTuple:
271282
t: Tuple
272283

273-
p = BareTuple((10, 20))
274-
assert p == de(BareTuple, se(p))
284+
c = BareTuple((10, 20))
285+
assert c == de(BareTuple, se(c))
286+
287+
c = BareTuple(())
288+
assert c == de(BareTuple, se(c))
275289

276290
@serde.serde(**opt)
291+
@dataclasses.dataclass
277292
class Nested:
278293
i: Tuple[data.Int, data.Int]
279294
s: Tuple[data.Str, data.Str]
@@ -282,13 +297,31 @@ class Nested:
282297

283298
# hmmm.. Nested tuple doesn't work ..
284299
if se is not serde.toml.to_toml:
285-
p = Nested(
300+
d = Nested(
286301
(data.Int(10), data.Int(20)),
287302
(data.Str("a"), data.Str("b")),
288303
(data.Float(10.0), data.Float(20.0)),
289304
(data.Bool(True), data.Bool(False)),
290305
)
291-
assert p == de(Nested, se(p))
306+
assert d == de(Nested, se(d))
307+
308+
@serde.serde(**opt)
309+
@dataclasses.dataclass
310+
class VariableTuple:
311+
t: Tuple[int, ...]
312+
313+
e = VariableTuple((1, 2, 3))
314+
assert e == de(VariableTuple, se(e))
315+
316+
e = VariableTuple(())
317+
assert e == de(VariableTuple, se(e))
318+
319+
with pytest.raises(Exception):
320+
321+
@serde.serde(**opt)
322+
@dataclasses.dataclass
323+
class EmptyTuple:
324+
t: Tuple[()]
292325

293326

294327
@pytest.mark.parametrize('se,de', all_formats)
@@ -809,6 +842,11 @@ def __post_init__(self):
809842
(Tuple[int], (10.0,), True),
810843
(Tuple[int, str], (10, "foo"), False),
811844
(Tuple[int, str], (10, 10.0), True),
845+
(Tuple[data.Int, data.Str], (data.Int(1), data.Str("2")), False),
846+
(Tuple[data.Int, data.Str], (data.Int(1), data.Int(2)), True),
847+
(Tuple, (10, 10.0), False),
848+
(Tuple[int, ...], (1, 2), False),
849+
(Tuple[int, ...], (1, 2.0), True),
812850
(data.E, data.E.S, False),
813851
(data.E, data.IE.V0, False), # TODO enum type check is not yet perfect
814852
(Union[int, str], 10, False),

tests/test_compat.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ class Foo:
204204
assert is_instance((10, "a"), tuple)
205205
assert is_instance((10, 'foo', 100.0, True), Tuple[int, str, float, bool])
206206
assert not is_instance((10, 'foo', 100.0, "last-type-is-wrong"), Tuple[int, str, float, bool])
207-
assert is_instance((10, "a"), Tuple[int, ...])
207+
assert is_instance((10, 20), Tuple[int, ...])
208+
assert is_instance((10, 20, 30), Tuple[int, ...])
209+
assert is_instance((), Tuple[int, ...])
210+
assert not is_instance((10, "a"), Tuple[int, ...])
208211

209212
# Tuple of dataclasses
210213
assert is_instance((Int(10), Str('foo'), Float(100.0), Bool(True)), Tuple[Int, Str, Float, Bool])

0 commit comments

Comments
 (0)