From b75276e16fb52b3862b554a384841c5488099d16 Mon Sep 17 00:00:00 2001 From: Dawid Kraczkowski Date: Wed, 8 Nov 2023 06:43:01 +0100 Subject: [PATCH] Support private properties, fix bug with p/c serialisables --- chili/decoder.py | 106 +++++++++------- chili/encoder.py | 104 ++++++++-------- chili/serializer.py | 12 +- chili/typing.py | 2 + poetry.lock | 20 ++-- pyproject.toml | 4 +- tests/test_decoder.py | 3 - .../custom_object_typehint_decoding_test.py | 113 ++++++++++++++++++ .../private_properties_support_test.py | 65 ++++++++++ .../test_parent_child_serialisation.py | 37 ++++++ 10 files changed, 353 insertions(+), 113 deletions(-) create mode 100644 tests/usecases/custom_object_typehint_decoding_test.py create mode 100644 tests/usecases/private_properties_support_test.py create mode 100644 tests/usecases/test_parent_child_serialisation.py diff --git a/chili/decoder.py b/chili/decoder.py index f881b33..3475b49 100644 --- a/chili/decoder.py +++ b/chili/decoder.py @@ -74,14 +74,23 @@ def decode(self, value): @final -class ProxyDecoder(Generic[T]): - def __init__(self, func: Callable[[Any], T]): +class SimpleDecoder(Generic[T]): + def __init__(self, func: Callable[[Any], T]) -> None: self._decoder = func def decode(self, value: Any) -> T: return self._decoder(value) +@final +class ProxyDecoder(Generic[T]): + def __init__(self, type_annotation: Any) -> None: + self._decoder = build_type_decoder(type_annotation) + + def decode(self, value: Any) -> T: + return self._decoder.decode(value) + + _REGEX_FLAGS = { "i": re.I, "m": re.M, @@ -158,44 +167,44 @@ def ordered_dict(value: List[List[Any]]) -> collections.OrderedDict: _builtin_type_decoders = TypeDecoders( { - bool: ProxyDecoder[bool](bool), - int: ProxyDecoder[int](int), - float: ProxyDecoder[float](float), - str: ProxyDecoder[str](str), - bytes: ProxyDecoder[bytes](lambda value: b64decode(value.encode("utf8"))), - bytearray: ProxyDecoder[bytearray](lambda value: bytearray(b64decode(value.encode("utf8")))), - list: ProxyDecoder[list](list), - set: ProxyDecoder[set](set), - frozenset: ProxyDecoder[frozenset](frozenset), - tuple: ProxyDecoder[tuple](tuple), - dict: ProxyDecoder[dict](dict), - collections.OrderedDict: ProxyDecoder[collections.OrderedDict](ordered_dict), - collections.deque: ProxyDecoder[collections.deque](collections.deque), - typing.TypedDict: ProxyDecoder[typing.TypedDict](typing.TypedDict), # type: ignore - typing.Dict: ProxyDecoder[dict](dict), - typing.List: ProxyDecoder[list](list), - typing.Sequence: ProxyDecoder[list](list), - typing.Tuple: ProxyDecoder[tuple](tuple), # type: ignore - typing.Set: ProxyDecoder[set](set), - typing.FrozenSet: ProxyDecoder[frozenset](frozenset), - typing.Deque: ProxyDecoder[typing.Deque](typing.Deque), - typing.AnyStr: ProxyDecoder[str](str), # type: ignore - decimal.Decimal: ProxyDecoder[decimal.Decimal](decimal.Decimal), - datetime.time: ProxyDecoder[datetime.time](parse_iso_time), - datetime.date: ProxyDecoder[datetime.date](parse_iso_date), - datetime.datetime: ProxyDecoder[datetime.datetime](parse_iso_datetime), - datetime.timedelta: ProxyDecoder[datetime.timedelta](parse_iso_duration), - PurePath: ProxyDecoder[PurePath](PurePath), - PureWindowsPath: ProxyDecoder[PureWindowsPath](PureWindowsPath), - PurePosixPath: ProxyDecoder[PurePosixPath](PurePosixPath), - Path: ProxyDecoder[Path](Path), - PosixPath: ProxyDecoder[PosixPath](PosixPath), - WindowsPath: ProxyDecoder[WindowsPath](WindowsPath), - Pattern: ProxyDecoder[Pattern](decode_regex_from_string), - re.Pattern: ProxyDecoder[re.Pattern](decode_regex_from_string), - IPv4Address: ProxyDecoder[IPv4Address](IPv4Address), - IPv6Address: ProxyDecoder[IPv6Address](IPv6Address), - UUID: ProxyDecoder[UUID](UUID), + bool: SimpleDecoder[bool](bool), + int: SimpleDecoder[int](int), + float: SimpleDecoder[float](float), + str: SimpleDecoder[str](str), + bytes: SimpleDecoder[bytes](lambda value: b64decode(value.encode("utf8"))), + bytearray: SimpleDecoder[bytearray](lambda value: bytearray(b64decode(value.encode("utf8")))), + list: SimpleDecoder[list](list), + set: SimpleDecoder[set](set), + frozenset: SimpleDecoder[frozenset](frozenset), + tuple: SimpleDecoder[tuple](tuple), + dict: SimpleDecoder[dict](dict), + collections.OrderedDict: SimpleDecoder[collections.OrderedDict](ordered_dict), + collections.deque: SimpleDecoder[collections.deque](collections.deque), + typing.TypedDict: SimpleDecoder[typing.TypedDict](typing.TypedDict), # type: ignore + typing.Dict: SimpleDecoder[dict](dict), + typing.List: SimpleDecoder[list](list), + typing.Sequence: SimpleDecoder[list](list), + typing.Tuple: SimpleDecoder[tuple](tuple), # type: ignore + typing.Set: SimpleDecoder[set](set), + typing.FrozenSet: SimpleDecoder[frozenset](frozenset), + typing.Deque: SimpleDecoder[typing.Deque](typing.Deque), + typing.AnyStr: SimpleDecoder[str](str), # type: ignore + decimal.Decimal: SimpleDecoder[decimal.Decimal](decimal.Decimal), + datetime.time: SimpleDecoder[datetime.time](parse_iso_time), + datetime.date: SimpleDecoder[datetime.date](parse_iso_date), + datetime.datetime: SimpleDecoder[datetime.datetime](parse_iso_datetime), + datetime.timedelta: SimpleDecoder[datetime.timedelta](parse_iso_duration), + PurePath: SimpleDecoder[PurePath](PurePath), + PureWindowsPath: SimpleDecoder[PureWindowsPath](PureWindowsPath), + PurePosixPath: SimpleDecoder[PurePosixPath](PurePosixPath), + Path: SimpleDecoder[Path](Path), + PosixPath: SimpleDecoder[PosixPath](PosixPath), + WindowsPath: SimpleDecoder[WindowsPath](WindowsPath), + Pattern: SimpleDecoder[Pattern](decode_regex_from_string), + re.Pattern: SimpleDecoder[re.Pattern](decode_regex_from_string), + IPv4Address: SimpleDecoder[IPv4Address](IPv4Address), + IPv6Address: SimpleDecoder[IPv6Address](IPv6Address), + UUID: SimpleDecoder[UUID](UUID), } ) @@ -321,7 +330,7 @@ class ClassDecoder(TypeDecoder): def __init__(self, class_name: Type, extra_decoders: TypeDecoders = None): self.class_name = class_name - self._schema = create_schema(class_name) + self._schema = create_schema(class_name) # type: ignore self._extra_decoders = extra_decoders def decode(self, value: StateObject) -> Any: @@ -440,7 +449,9 @@ def decode(self, value: Any) -> Any: @lru_cache(maxsize=None) -def build_type_decoder(a_type: Type, extra_decoders: TypeDecoders = None, module: Any = None) -> TypeDecoder: +def build_type_decoder( + a_type: Type, extra_decoders: TypeDecoders = None, module: Any = None, force: bool = False +) -> TypeDecoder: if extra_decoders and a_type in extra_decoders: return extra_decoders[a_type] @@ -472,7 +483,7 @@ def build_type_decoder(a_type: Type, extra_decoders: TypeDecoders = None, module return TypedDictDecoder(origin_type, extra_decoders) if is_class(origin_type) and is_user_string(origin_type): - return ProxyDecoder[origin_type](origin_type) # type: ignore + return SimpleDecoder[origin_type](origin_type) # type: ignore if origin_type is Union: type_args = get_type_args(a_type) @@ -505,6 +516,8 @@ def build_type_decoder(a_type: Type, extra_decoders: TypeDecoders = None, module return OptionalTypeDecoder(build_type_decoder(unpack_optional(a_type))) # type: ignore if origin_type not in _supported_generics: + if force and is_class(origin_type): + return Decoder[origin_type](extra_decoders) # type: ignore raise DecoderError.invalid_type(a_type) type_attributes: List[Union[TypeDecoder, Any]] = [ @@ -551,7 +564,10 @@ def decode(self, obj: Dict[str, StateObject]) -> T: else: value = self._decoders[prop.name].decode(obj[key]) - setattr(instance, prop.name, value) + try: + setattr(instance, prop.name, value) + except AttributeError: + setattr(instance, f"_{prop.name}", value) return instance @@ -559,7 +575,7 @@ def _build_decoders(self) -> Dict[str, TypeDecoder]: schema: TypeSchema = getattr(self.__generic__, _PROPERTIES) return { - prop.name: build_type_decoder(prop.type, extra_decoders=self.type_decoders) # type: ignore + prop.name: build_type_decoder(prop.type, extra_decoders=self.type_decoders, force=True) # type: ignore for prop in schema.values() } diff --git a/chili/encoder.py b/chili/encoder.py index ad5dfda..7d603b3 100644 --- a/chili/encoder.py +++ b/chili/encoder.py @@ -57,7 +57,7 @@ def encode(self, value): @final -class ProxyEncoder(TypeEncoder, Generic[T]): +class SimpleEncoder(TypeEncoder, Generic[T]): def __init__(self, func: Callable[[Any], T]): self._encoder = func @@ -65,6 +65,15 @@ def encode(self, value: Any) -> T: return self._encoder(value) +@final +class ProxyEncoder(Generic[T]): + def __init__(self, type_annotation: Any) -> None: + self._encoder = build_type_encoder(type_annotation) + + def decode(self, value: Any) -> T: + return self._encoder.encode(value) + + def encode_regex_to_string(value: Pattern) -> str: """ Encodes regex into string and preserves flags if they are set. Then regex is normally wrapped between slashes. @@ -126,44 +135,44 @@ def ordered_dict(value: collections.OrderedDict) -> List[List[Any]]: _builtin_type_encoders = TypeEncoders( { - bool: ProxyEncoder[bool](bool), - int: ProxyEncoder[int](int), - float: ProxyEncoder[float](float), - str: ProxyEncoder[str](str), - bytes: ProxyEncoder[str](lambda value: b64encode(value).decode("utf8")), - bytearray: ProxyEncoder[str](lambda value: b64encode(value).decode("utf8")), - list: ProxyEncoder[list](list), - set: ProxyEncoder[list](list), - frozenset: ProxyEncoder[list](list), - tuple: ProxyEncoder[list](list), - dict: ProxyEncoder[dict](dict), - collections.OrderedDict: ProxyEncoder[list](ordered_dict), - collections.deque: ProxyEncoder[list](list), - typing.TypedDict: ProxyEncoder[dict](dict), # type: ignore - typing.Dict: ProxyEncoder[dict](dict), - typing.List: ProxyEncoder[list](list), - typing.Sequence: ProxyEncoder[list](list), - typing.Tuple: ProxyEncoder[list](list), # type: ignore - typing.Set: ProxyEncoder[list](list), - typing.FrozenSet: ProxyEncoder[list](list), - typing.Deque: ProxyEncoder[list](list), - typing.AnyStr: ProxyEncoder[str](str), # type: ignore - decimal.Decimal: ProxyEncoder[str](str), - datetime.time: ProxyEncoder[str](lambda value: value.isoformat()), - datetime.date: ProxyEncoder[str](lambda value: value.isoformat()), - datetime.datetime: ProxyEncoder[str](lambda value: value.isoformat()), - datetime.timedelta: ProxyEncoder[str](timedelta_to_iso_duration), - PurePath: ProxyEncoder[str](str), - PureWindowsPath: ProxyEncoder[str](str), - PurePosixPath: ProxyEncoder[str](str), - Path: ProxyEncoder[str](str), - PosixPath: ProxyEncoder[str](str), - WindowsPath: ProxyEncoder[str](str), - Pattern: ProxyEncoder[str](encode_regex_to_string), - re.Pattern: ProxyEncoder[str](encode_regex_to_string), - IPv6Address: ProxyEncoder[str](str), - IPv4Address: ProxyEncoder[str](str), - UUID: ProxyEncoder[str](str), + bool: SimpleEncoder[bool](bool), + int: SimpleEncoder[int](int), + float: SimpleEncoder[float](float), + str: SimpleEncoder[str](str), + bytes: SimpleEncoder[str](lambda value: b64encode(value).decode("utf8")), + bytearray: SimpleEncoder[str](lambda value: b64encode(value).decode("utf8")), + list: SimpleEncoder[list](list), + set: SimpleEncoder[list](list), + frozenset: SimpleEncoder[list](list), + tuple: SimpleEncoder[list](list), + dict: SimpleEncoder[dict](dict), + collections.OrderedDict: SimpleEncoder[list](ordered_dict), + collections.deque: SimpleEncoder[list](list), + typing.TypedDict: SimpleEncoder[dict](dict), # type: ignore + typing.Dict: SimpleEncoder[dict](dict), + typing.List: SimpleEncoder[list](list), + typing.Sequence: SimpleEncoder[list](list), + typing.Tuple: SimpleEncoder[list](list), # type: ignore + typing.Set: SimpleEncoder[list](list), + typing.FrozenSet: SimpleEncoder[list](list), + typing.Deque: SimpleEncoder[list](list), + typing.AnyStr: SimpleEncoder[str](str), # type: ignore + decimal.Decimal: SimpleEncoder[str](str), + datetime.time: SimpleEncoder[str](lambda value: value.isoformat()), + datetime.date: SimpleEncoder[str](lambda value: value.isoformat()), + datetime.datetime: SimpleEncoder[str](lambda value: value.isoformat()), + datetime.timedelta: SimpleEncoder[str](timedelta_to_iso_duration), + PurePath: SimpleEncoder[str](str), + PureWindowsPath: SimpleEncoder[str](str), + PurePosixPath: SimpleEncoder[str](str), + Path: SimpleEncoder[str](str), + PosixPath: SimpleEncoder[str](str), + WindowsPath: SimpleEncoder[str](str), + Pattern: SimpleEncoder[str](encode_regex_to_string), + re.Pattern: SimpleEncoder[str](encode_regex_to_string), + IPv6Address: SimpleEncoder[str](str), + IPv4Address: SimpleEncoder[str](str), + UUID: SimpleEncoder[str](str), } ) @@ -227,7 +236,7 @@ class ClassEncoder(TypeEncoder): def __init__(self, class_name: Type, extra_encoders: TypeEncoders = None): self.class_name = class_name self._extra_encoders = extra_encoders - self._schema = create_schema(class_name) + self._schema = create_schema(class_name) # type: ignore def encode(self, value: Any) -> StateObject: if not isinstance(value, self.class_name): @@ -349,7 +358,9 @@ def encode(self, value: Any) -> Any: @lru_cache(maxsize=None) -def build_type_encoder(a_type: Type, extra_encoders: TypeEncoders = None, module: Any = None) -> TypeEncoder: +def build_type_encoder( + a_type: Type, extra_encoders: TypeEncoders = None, module: Any = None, force: bool = False +) -> TypeEncoder: if extra_encoders and a_type in extra_encoders: return extra_encoders[a_type] @@ -381,7 +392,7 @@ def build_type_encoder(a_type: Type, extra_encoders: TypeEncoders = None, module return TypedDictEncoder(origin_type, extra_encoders) if is_class(origin_type) and is_user_string(origin_type): - return ProxyEncoder[str](str) + return SimpleEncoder[str](str) if origin_type is Union: type_args = get_type_args(a_type) @@ -402,9 +413,6 @@ def build_type_encoder(a_type: Type, extra_encoders: TypeEncoders = None, module return GenericClassEncoder(a_type) return Encoder[origin_type](encoders=extra_encoders) # type: ignore[valid-type] - if is_optional(a_type): - return OptionalTypeEncoder(build_type_encoder(unpack_optional(a_type), extra_encoders)) # type: ignore - if isinstance(a_type, TypeVar): if a_type.__bound__ is None: raise EncoderError.invalid_type(a_type) @@ -414,7 +422,9 @@ def build_type_encoder(a_type: Type, extra_encoders: TypeEncoders = None, module return build_type_encoder(a_type.__supertype__, extra_encoders, module) if origin_type not in _supported_generics: - raise EncoderError.invalid_type(a_type) + if is_class(origin_type) and force: + return Encoder[origin_type](encoders=extra_encoders) # type: ignore[valid-type] + raise EncoderError.invalid_type(type=a_type) type_attributes: List[TypeEncoder] = [ build_type_encoder(subtype, extra_encoders=extra_encoders, module=module) # type: ignore @@ -467,7 +477,7 @@ def _build_encoders(self) -> Dict[str, TypeEncoder]: schema: TypeSchema = self.schema return { - prop.name: build_type_encoder(prop.type, extra_encoders=self.type_encoders) # type: ignore + prop.name: build_type_encoder(prop.type, extra_encoders=self.type_encoders, force=True) # type: ignore for prop in schema.values() } diff --git a/chili/serializer.py b/chili/serializer.py index db49f26..c40f637 100644 --- a/chili/serializer.py +++ b/chili/serializer.py @@ -50,12 +50,12 @@ def __class_getitem__(cls, item: Type[T]) -> Type[Serializer]: # noqa: E501 def serializable(_cls=None, in_mapper: Optional[Mapper] = None, out_mapper: Optional[Mapper] = None) -> Any: def _decorate(cls) -> Type[C]: - if not hasattr(cls, _PROPERTIES): - setattr(cls, _PROPERTIES, create_schema(cls)) - if in_mapper is not None: - setattr(cls, _DECODE_MAPPER, in_mapper) - if out_mapper is not None: - setattr(cls, _ENCODE_MAPPER, out_mapper) + + setattr(cls, _PROPERTIES, create_schema(cls)) + if in_mapper is not None: + setattr(cls, _DECODE_MAPPER, in_mapper) + if out_mapper is not None: + setattr(cls, _ENCODE_MAPPER, out_mapper) setattr(cls, _DECODABLE, True) setattr(cls, _ENCODABLE, True) diff --git a/chili/typing.py b/chili/typing.py index 0f21306..2712177 100644 --- a/chili/typing.py +++ b/chili/typing.py @@ -5,6 +5,7 @@ from collections import UserString from dataclasses import MISSING, Field, InitVar, is_dataclass from enum import Enum +from functools import lru_cache from inspect import isclass as is_class from typing import Any, Callable, ClassVar, Dict, List, NewType, Optional, Type, Union @@ -184,6 +185,7 @@ def __eq__(self, other: Property) -> bool: # type: ignore _default_factories = (list, dict, tuple, set, bytes, bytearray, frozenset) +@lru_cache def create_schema(cls: Type) -> TypeSchema: try: properties = typing.get_type_hints(cls, localns=cls.__dict__) # type: ignore diff --git a/poetry.lock b/poetry.lock index 3917c33..32cafb9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -187,13 +187,13 @@ test = ["pytest (>=6)"] [[package]] name = "gaffe" -version = "0.2.2" +version = "0.3.0" description = "Simple structured exceptions for python." optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "gaffe-0.2.2-py3-none-any.whl", hash = "sha256:3d3069dc19348dd3ef84fd4b6af9f8a68d91d63fac2e66abae46146a98340699"}, - {file = "gaffe-0.2.2.tar.gz", hash = "sha256:bdcf88cb35eba7ce81073e80f48ee14859c31118b5ffc1c3d4637c5a4622fa7b"}, + {file = "gaffe-0.3.0-py3-none-any.whl", hash = "sha256:fce5ad7cc5b2b6596775220db88ad54767cb32e5d589dfe1ca356ae7dc5c15e9"}, + {file = "gaffe-0.3.0.tar.gz", hash = "sha256:1c09015fc8ff0343e8c94e37cfc4d24b92dd6c2e0e7cba87bea210eeab68ee3f"}, ] [[package]] @@ -416,13 +416,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -524,13 +524,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.1" +version = "0.12.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, - {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, + {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, + {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, ] [[package]] @@ -631,4 +631,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "4797c68511996d9895d2ee076e46e286f7bbef98755106eb5eb54df4b3aec0fe" +content-hash = "77e29e034f7e6ab5b681763b78f66509807b44da463781fb9f44ec7a26350f2e" diff --git a/pyproject.toml b/pyproject.toml index 18a46eb..87db310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,10 +22,10 @@ license = "MIT" name = "chili" readme = "README.md" repository = "https://github.com/kodemore/chili" -version = "2.5.0" +version = "2.6.0" [tool.poetry.dependencies] -gaffe = "^0.2.1" +gaffe = ">=0.3.0" python = "^3.8" typing-extensions = "^4.2" diff --git a/tests/test_decoder.py b/tests/test_decoder.py index 8e46b1d..fe24d18 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -1,7 +1,4 @@ -import pytest - from chili import Decoder, decodable -from chili.error import DecoderError def test_can_instantiate() -> None: diff --git a/tests/usecases/custom_object_typehint_decoding_test.py b/tests/usecases/custom_object_typehint_decoding_test.py new file mode 100644 index 0000000..ff3f35f --- /dev/null +++ b/tests/usecases/custom_object_typehint_decoding_test.py @@ -0,0 +1,113 @@ +from chili import Decoder, Encoder + + +def test_can_decode_complex_typehinted_object() -> None: + # given + class PetName: + value: str + + def __init__(self, value: str) -> None: + self.value = value + + class PetAddress: + street_name: str + city: str + + def __init__(self, street_name: str, city: str): + self._street_name = street_name + self._city = city + + @property + def street_name(self): + return self._street_name + + @property + def city(self): + return self._city + + class Pet: + address: PetAddress + name: PetName + + def __init__(self, name: PetName, address: PetAddress) -> None: + self._address = address + self._name = name + + @property + def address(self) -> PetAddress: + return self._address + + @property + def name(self) -> PetName: + return self._name + + data = { + "name": { + "value": "Bobik", + }, + "address": { + "city": "Bobikowo", + "street_name": "Bobiczna 69", + }, + } + + decoder = Decoder[Pet]() + + # when + pet = decoder.decode(data) + + # then + assert isinstance(pet, Pet) + assert isinstance(pet.address, PetAddress) + assert isinstance(pet.name, PetName) + + +def test_can_encode_complex_typehinted_object() -> None: + # given + class PetName: + value: str + + def __init__(self, value: str) -> None: + self.value = value + + class PetAddress: + street_name: str + city: str + + def __init__(self, street_name: str, city: str): + self._street_name = street_name + self._city = city + + @property + def street_name(self): + return self._street_name + + @property + def city(self): + return self._city + + class Pet: + address: PetAddress + name: PetName + + def __init__(self, name: PetName, address: PetAddress) -> None: + self._address = address + self._name = name + + @property + def address(self) -> PetAddress: + return self._address + + @property + def name(self) -> PetName: + return self._name + + pet = Pet(PetName("Bobik"), PetAddress("Bobiczna 69", "Bobikowo")) + + encoder = Encoder[Pet]() + + # when + data = encoder.encode(pet) + + # then + assert data == {"address": {"street_name": "Bobiczna 69", "city": "Bobikowo"}, "name": {"value": "Bobik"}} diff --git a/tests/usecases/private_properties_support_test.py b/tests/usecases/private_properties_support_test.py new file mode 100644 index 0000000..f8e9db3 --- /dev/null +++ b/tests/usecases/private_properties_support_test.py @@ -0,0 +1,65 @@ +from chili import Decoder, Encoder + + +def test_can_encode_private_property() -> None: + # given + class Pet: + age: int + name: str + + def __init__(self, name: str, age: int) -> None: + self._name = name + self._age = age + + @property + def name(self) -> str: + return self._name + + @property + def age(self) -> int: + return self._age + + encoder = Encoder[Pet]() + + # when + data = encoder.encode(Pet("Bobik", 3)) + + # then + assert data == { + "age": 3, + "name": "Bobik", + } + + +def test_can_decode_private_property() -> None: + # given + class Pet: + age: int + name: str + + def __init__(self, name: str, age: int) -> None: + self._name = name + self._age = age + + @property + def name(self) -> str: + return self._name + + @property + def age(self) -> int: + return self._age + + encoder = Decoder[Pet]() + + # when + pet = encoder.decode( + { + "age": 3, + "name": "Bobik", + } + ) + + # then + assert isinstance(pet, Pet) + + print(pet) diff --git a/tests/usecases/test_parent_child_serialisation.py b/tests/usecases/test_parent_child_serialisation.py new file mode 100644 index 0000000..6890a41 --- /dev/null +++ b/tests/usecases/test_parent_child_serialisation.py @@ -0,0 +1,37 @@ +from chili import encode, serializable + + +def test_can_encode_parent_child() -> None: + # given + @serializable + class Parent: + foo: int + + def __init__(self, foo): + self.foo = foo + + def some_logic(self): + pass + + @serializable + class Child(Parent): + bar: float + + def __init__(self, foo, bar): + super().__init__(foo) + self.bar = bar + + def more_logic(self): + pass + + parent_obj = Parent(5) + child_obj = Child(7, 0.2) + + # when + encoded_parent = encode(parent_obj) + encoded_child = encode(child_obj) + + # then + assert encoded_child == {"bar": 0.2, "foo": 7} + + assert encoded_parent == {"foo": 5}