| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2022 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from dataclasses import dataclass, field |
| import json |
| from typing import Dict, List, Optional, Set, Union |
| import unittest |
| |
| from serialization import ( |
| instance_to_dict, |
| instance_from_dict, |
| serialize_dict, |
| serialize_json, |
| serialize_fields_as, |
| ) |
| |
| |
| class SerializeFieldsTest(unittest.TestCase): |
| """Validate that fields of different kinds are properly serialized.""" |
| |
| def test_serialize_simple_class(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClass(42, "a string") |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"int_field": 42, "str_field": "a string"}, |
| ) |
| |
| def test_deserialize_simple_class(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| value = {"int_field": 42, "str_field": "a string"} |
| self.assertEqual( |
| instance_from_dict(SimpleClass, value), SimpleClass(42, "a string") |
| ) |
| |
| def test_deserialize_simple_class_with_default_value(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str = "a default value" |
| |
| value = {"int_field": 42} |
| self.assertEqual( |
| instance_from_dict(SimpleClass, value), |
| SimpleClass(42, "a default value"), |
| ) |
| |
| def test_deserialize_simple_class_with_default_factory(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: List[str] = field(default_factory=list) |
| |
| value = {"int_field": 42} |
| self.assertEqual( |
| instance_from_dict(SimpleClass, value), SimpleClass(42, []) |
| ) |
| |
| def test_serialize_int_value_0(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClass(0, "a string") |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"int_field": 0, "str_field": "a string"}, |
| ) |
| |
| def test_serialize_optional_field_with_value(self): |
| @dataclass |
| class SimpleClassWithOptionalField: |
| int_field: Optional[int] |
| str_field: str |
| |
| instance = SimpleClassWithOptionalField(21, "some value") |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"int_field": 21, "str_field": "some value"}, |
| ) |
| |
| def test_serialize_optional_field_without_value(self): |
| @dataclass |
| class SimpleClassWithOptionalField: |
| int_field: Optional[int] |
| str_field: str |
| |
| instance = SimpleClassWithOptionalField(None, "some value") |
| self.assertEqual( |
| instance_to_dict(instance), {"str_field": "some value"} |
| ) |
| |
| def test_serialize_list_fields(self): |
| @dataclass |
| class SimpleClassWithList: |
| int_field: List[int] = field(default_factory=list) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithList([1, 2, 3, 4, 5]) |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"int_field": [1, 2, 3, 4, 5], "str_field": "foo"}, |
| ) |
| |
| def test_serialize_empty_list_fields(self): |
| @dataclass |
| class SimpleClassWithList: |
| int_field: List[int] = field(default_factory=list) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithList() |
| self.assertEqual( |
| instance_to_dict(instance), {"int_field": [], "str_field": "foo"} |
| ) |
| |
| def test_deserialize_list_fields(self): |
| @dataclass |
| class SimpleClassWithList: |
| int_field: List[int] = field(default_factory=list) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithList([1, 2, 3, 4, 5]) |
| self.assertEqual( |
| instance_from_dict( |
| SimpleClassWithList, |
| {"int_field": [1, 2, 3, 4, 5], "str_field": "foo"}, |
| ), |
| instance, |
| ) |
| |
| def test_deserialize_empty_list_fields(self): |
| @dataclass |
| class SimpleClassWithList: |
| int_field: List[int] = field(default_factory=list) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithList() |
| self.assertEqual( |
| instance_from_dict( |
| SimpleClassWithList, {"int_field": [], "str_field": "foo"} |
| ), |
| instance, |
| ) |
| |
| def test_deserialize_missing_list_fields_empty(self): |
| @dataclass |
| class SimpleClassWithList: |
| int_field: List[int] = field(default_factory=list) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithList() |
| self.assertEqual( |
| instance_from_dict(SimpleClassWithList, {"str_field": "foo"}), |
| instance, |
| ) |
| |
| def test_serialize_set_fields(self): |
| # Note that this also tests that sets are serialized into sorted-order |
| @dataclass |
| class SimpleClassWithSet: |
| int_field: Set[int] = field(default_factory=set) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithSet(set([5, 4, 3, 2, 1])) |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"int_field": [1, 2, 3, 4, 5], "str_field": "foo"}, |
| ) |
| |
| def test_serialize_empty_set_fields(self): |
| @dataclass |
| class SimpleClassWithSet: |
| int_field: Set[int] = field(default_factory=set) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithSet() |
| self.assertEqual( |
| instance_to_dict(instance), {"int_field": [], "str_field": "foo"} |
| ) |
| |
| def test_deserialize_set_fields(self): |
| @dataclass |
| class SimpleClassWithSet: |
| int_field: Set[int] = field(default_factory=set) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithSet(set([5, 4, 3, 2, 1])) |
| self.assertEqual( |
| instance_from_dict( |
| SimpleClassWithSet, |
| {"int_field": [1, 2, 3, 4, 5], "str_field": "foo"}, |
| ), |
| instance, |
| ) |
| |
| def test_deserialize_empty_set_fields(self): |
| @dataclass |
| class SimpleClassWithSet: |
| int_field: Set[int] = field(default_factory=set) |
| str_field: str = "foo" |
| |
| instance = SimpleClassWithSet() |
| self.assertEqual( |
| instance_from_dict( |
| SimpleClassWithSet, {"int_field": [], "str_field": "foo"} |
| ), |
| instance, |
| ) |
| |
| def test_deserialize_missing_set_fields_empty(self): |
| @dataclass |
| class SimpleClassWithSet: |
| str_field: str = "foo" |
| int_field: Set[int] = field(default_factory=set) |
| |
| instance = SimpleClassWithSet() |
| self.assertEqual( |
| instance_from_dict(SimpleClassWithSet, {"str_field": "foo"}), |
| instance, |
| ) |
| |
| def test_serialize_dict_fields(self): |
| @dataclass |
| class SimpleClassWithDict: |
| dict_field: Dict[str, int] |
| |
| instance = SimpleClassWithDict({"one": 1, "two": 2, "three": 3}) |
| self.assertEqual( |
| instance_to_dict(instance), |
| {"dict_field": {"one": 1, "two": 2, "three": 3}}, |
| ) |
| |
| def test_serialize_fields_as(self): |
| @dataclass |
| @serialize_fields_as(int_field=str) |
| class SimpleClassWithMetdata: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClassWithMetdata(7, "a string") |
| self.assertEqual( |
| instance_to_dict(instance), |
| { |
| "int_field": "7", |
| "str_field": "a string", |
| }, |
| ) |
| |
| def test_serialize_fields_as_with_callable(self): |
| def my_serializer(value: int) -> str: |
| return f"The value is {value}." |
| |
| @dataclass |
| @serialize_fields_as(int_field=my_serializer) |
| class SimpleClassWithMetdata: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClassWithMetdata(7, "a string") |
| self.assertEqual( |
| instance_to_dict(instance), |
| { |
| "int_field": "The value is 7.", |
| "str_field": "a string", |
| }, |
| ) |
| |
| def test_serialize_class_with_superclass(self): |
| @dataclass |
| class SimpleBaseClass: |
| int_field_base: int |
| str_field_base: str |
| |
| @dataclass |
| class SimpleChildClass(SimpleBaseClass): |
| int_field_child: int |
| str_field_child: str |
| |
| instance = SimpleChildClass( |
| int_field_base=42, |
| str_field_base="base", |
| int_field_child=84, |
| str_field_child="child", |
| ) |
| self.assertEqual( |
| instance_to_dict(instance), |
| { |
| "int_field_base": 42, |
| "str_field_base": "base", |
| "int_field_child": 84, |
| "str_field_child": "child", |
| }, |
| ) |
| |
| def test_deserialize_class_with_superclass(self): |
| @dataclass |
| class SimpleBaseClass: |
| int_field_base: int |
| str_field_base: str |
| |
| @dataclass |
| class SimpleChildClass(SimpleBaseClass): |
| int_field_child: int |
| str_field_child: str |
| |
| instance = SimpleChildClass( |
| int_field_base=42, |
| str_field_base="base", |
| int_field_child=84, |
| str_field_child="child", |
| ) |
| |
| self.assertEqual( |
| instance_from_dict( |
| SimpleChildClass, |
| { |
| "int_field_base": 42, |
| "str_field_base": "base", |
| "int_field_child": 84, |
| "str_field_child": "child", |
| }, |
| ), |
| instance, |
| ) |
| |
| def test_serialize_class_with_multiple_superclasses(self): |
| @dataclass |
| class RootClass: |
| int_field_root: int |
| |
| @dataclass |
| class SimpleBaseClass(RootClass): |
| int_field_base: int |
| str_field_base: str |
| |
| @dataclass |
| class AnotherBaseClass: |
| str_field_another: str |
| |
| @dataclass |
| class SimpleChildClass(SimpleBaseClass, AnotherBaseClass): |
| int_field_child: int |
| str_field_child: str |
| |
| instance = SimpleChildClass( |
| int_field_root=89, |
| int_field_base=42, |
| str_field_base="base", |
| str_field_another="another", |
| int_field_child=84, |
| str_field_child="child", |
| ) |
| |
| self.assertEqual( |
| instance_to_dict(instance), |
| { |
| "int_field_root": 89, |
| "int_field_base": 42, |
| "str_field_base": "base", |
| "str_field_another": "another", |
| "int_field_child": 84, |
| "str_field_child": "child", |
| }, |
| ) |
| |
| def test_deserialize_class_with_multiple_superclasses(self): |
| @dataclass |
| class RootClass: |
| int_field_root: int |
| |
| @dataclass |
| class SimpleBaseClass(RootClass): |
| int_field_base: int |
| str_field_base: str |
| |
| @dataclass |
| class AnotherBaseClass: |
| str_field_another: str |
| |
| @dataclass |
| class SimpleChildClass(SimpleBaseClass, AnotherBaseClass): |
| int_field_child: int |
| str_field_child: str |
| |
| instance = SimpleChildClass( |
| int_field_root=89, |
| int_field_base=42, |
| str_field_base="base", |
| str_field_another="another", |
| int_field_child=84, |
| str_field_child="child", |
| ) |
| |
| self.assertEqual( |
| instance_from_dict( |
| SimpleChildClass, |
| { |
| "int_field_root": 89, |
| "int_field_base": 42, |
| "str_field_base": "base", |
| "str_field_another": "another", |
| "int_field_child": 84, |
| "str_field_child": "child", |
| }, |
| ), |
| instance, |
| ) |
| |
| def test_deserialize_class_with_union_fields_first_type(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: Union[int, List[str]] |
| |
| self.assertEqual( |
| SimpleClass(42, 23), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": 23} |
| ), |
| ) |
| |
| def test_deserialize_class_with_union_fields_second_type(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: Union[int, List[str]] |
| |
| self.assertEqual( |
| SimpleClass(42, ["23"]), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": ["23"]} |
| ), |
| ) |
| |
| def test_deserialize_class_with_optional_fields_new_syntax_with_value(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str | None = None |
| |
| self.assertEqual( |
| SimpleClass(42, "foo"), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "str_field": "foo"} |
| ), |
| ) |
| |
| def test_deserialize_class_with_optional_fields_new_syntax_without_value( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str | None = None |
| |
| self.assertEqual( |
| SimpleClass(42), |
| instance_from_dict(SimpleClass, {"int_field": 42}), |
| ) |
| |
| def test_deserialize_class_with_optional_fields_new_syntax_with_explicit_none_value( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| str_field: str | None = None |
| |
| self.assertEqual( |
| SimpleClass(42), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "str_field": None} |
| ), |
| ) |
| |
| # Test when the field is not present |
| self.assertEqual( |
| SimpleClass(42), |
| instance_from_dict(SimpleClass, {"int_field": 42}), |
| ) |
| |
| def test_deserialize_class_with_union_fields_new_syntax_first_type(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: str | list[str] |
| |
| self.assertEqual( |
| SimpleClass(42, "foo"), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": "foo"} |
| ), |
| ) |
| |
| def test_deserialize_class_with_union_fields_new_syntax_second_type(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: int | list[str] |
| |
| self.assertEqual( |
| SimpleClass(42, ["foo"]), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": ["foo"]} |
| ), |
| ) |
| |
| def test_deserialize_class_with_union_fields_mixed_syntax_second_type(self): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: str | List[str] |
| |
| self.assertEqual( |
| SimpleClass(42, ["foo"]), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": ["foo"]} |
| ), |
| ) |
| |
| def test_deserialize_class_with_optional_union_fields_new_syntax_first_type( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: str | list[int] | None = None |
| |
| self.assertEqual( |
| SimpleClass(42, "foo"), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": "foo"} |
| ), |
| ) |
| |
| def test_deserialize_class_with_optional_union_fields_new_syntax_second_type( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: list[int] | str | None = None |
| |
| self.assertEqual( |
| SimpleClass(42, [23]), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": [23]} |
| ), |
| ) |
| |
| def test_deserialize_class_with_optional_union_fields_new_syntax_with_no_value( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: str | list[int] | None = None |
| |
| self.assertEqual( |
| SimpleClass(42), |
| instance_from_dict(SimpleClass, {"int_field": 42}), |
| ) |
| |
| def test_deserialize_class_with_optional_union_fields_new_syntax_with_explicit_none_value( |
| self, |
| ): |
| @dataclass |
| class SimpleClass: |
| int_field: int |
| union_field: str | list[int] | None = None |
| |
| self.assertEqual( |
| SimpleClass(42), |
| instance_from_dict( |
| SimpleClass, {"int_field": 42, "union_field": None} |
| ), |
| ) |
| |
| def test_serialize_nested_classes(self): |
| @dataclass |
| class Inner: |
| int_field: int |
| |
| @dataclass |
| class Outer: |
| inner: Inner |
| |
| self.assertEqual( |
| {"inner": {"int_field": 43}}, |
| instance_to_dict(Outer(Inner(43))), |
| ) |
| |
| def test_deserialize_nested_classes(self): |
| @dataclass |
| class Inner: |
| int_field: int |
| |
| @dataclass |
| class Outer: |
| inner: Inner |
| |
| self.assertEqual( |
| Outer(Inner(43)), |
| instance_from_dict(Outer, {"inner": {"int_field": 43}}), |
| ) |
| |
| |
| class SerializeToDictDecorator(unittest.TestCase): |
| """Validate that the `@serialize_to_dict class decorator behaves correctly. |
| |
| Note: These function correctly at runtime, but don't interact correctly with |
| PyRight, so they don't have proper static analysis and IDE type-checking. |
| """ |
| |
| def test_to_dict_decorator(self): |
| @dataclass |
| @serialize_dict |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClass(8, "some value") |
| self.assertEqual( |
| instance.to_dict(), {"int_field": 8, "str_field": "some value"} |
| ) |
| |
| def test_from_dict_decorator(self): |
| @dataclass |
| @serialize_dict |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| raw = {"int_field": 8, "str_field": "some value"} |
| self.assertEqual( |
| SimpleClass.from_dict(raw), SimpleClass(8, "some value") |
| ) |
| |
| |
| class SerializeToJsonDecorator(unittest.TestCase): |
| """Validate that the `@serialize_to_json class decorator behaves correctly. |
| |
| Note: These function correctly at runtime, but don't interact correctly with |
| PyRight, so they don't have proper static analysis and IDE type-checking. |
| """ |
| |
| def test_to_json_decorator(self): |
| @dataclass |
| @serialize_json |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClass(8, "some value") |
| result = instance.json_dumps(indent=6) |
| self.assertEqual( |
| result, |
| """{ |
| "int_field": 8, |
| "str_field": "some value" |
| }""", |
| ) |
| |
| def test_from_json_decorator(self): |
| @dataclass |
| @serialize_json |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| raw = {"int_field": 8, "str_field": "some value"} |
| raw_json = json.dumps(raw) |
| |
| self.assertEqual( |
| SimpleClass.json_loads(raw_json), SimpleClass(8, "some value") |
| ) |
| |
| def test_to_json_decorator_with_field_serializer(self): |
| def my_serializer(value: int) -> str: |
| return f"my value is {value}." |
| |
| @dataclass |
| @serialize_fields_as(int_field=my_serializer) |
| @serialize_json |
| class SimpleClass: |
| int_field: int |
| str_field: str |
| |
| instance = SimpleClass(8, "some value") |
| result = instance.json_dumps(indent=6) |
| self.assertEqual( |
| result, |
| """{ |
| "int_field": "my value is 8.", |
| "str_field": "some value" |
| }""", |
| ) |