Skip to content

Commit

Permalink
Support new types.UnionType (#35)
Browse files Browse the repository at this point in the history
* Support new types.UnionType
  • Loading branch information
dkraczkowski authored May 28, 2024
1 parent 248948e commit 6dae754
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 147 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v2
Expand Down
14 changes: 10 additions & 4 deletions chili/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def _build_type_decoder(self, a_type: Type) -> TypeDecoder:
map_generic_type(a_type, self._generic_parameters),
self._extra_decoders,
self._generic_type.__module__,
self.force
self.force,
)


Expand Down Expand Up @@ -444,7 +444,9 @@ def decode(self, value: list) -> tuple:
def _build(self) -> None:
field_types = self.class_name.__annotations__
for item_type in field_types.values():
self._arg_decoders.append(build_type_decoder(item_type, self._extra_decoders, self.class_name.__module__, self.force))
self._arg_decoders.append(
build_type_decoder(item_type, self._extra_decoders, self.class_name.__module__, self.force)
)


class TypedDictDecoder(TypeDecoder):
Expand All @@ -453,7 +455,9 @@ def __init__(self, class_name: Type, extra_decoders: TypeDecoders = None, force:
self._key_decoders = {}
self._extra_decoders = extra_decoders
for key_name, key_type in class_name.__annotations__.items():
self._key_decoders[key_name] = build_type_decoder(key_type, self._extra_decoders, class_name.__module__, force)
self._key_decoders[key_name] = build_type_decoder(
key_type, self._extra_decoders, class_name.__module__, force
)

def decode(self, value: dict) -> dict:
return {key: self._key_decoders[key].decode(item) for key, item in value.items()}
Expand Down Expand Up @@ -546,7 +550,9 @@ def build_type_decoder(
return Decoder[origin_type](decoders=extra_decoders) # type: ignore

if is_optional(a_type):
return OptionalTypeDecoder(build_type_decoder(unpack_optional(a_type), extra_decoders, module, force)) # type: ignore
return OptionalTypeDecoder(
build_type_decoder(unpack_optional(a_type), extra_decoders, module, force) # type: ignore
)

if origin_type not in _supported_generics:
if force and is_class(origin_type):
Expand Down
10 changes: 7 additions & 3 deletions chili/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def _build_type_encoder(self, a_type: Type) -> TypeEncoder:
map_generic_type(a_type, self._generic_parameters),
self._extra_encoders,
self._generic_type.__module__,
self.force
self.force,
)


Expand Down Expand Up @@ -318,7 +318,9 @@ def encode(self, value: tuple) -> list:
def _build(self) -> None:
field_types = self.type.__annotations__
for item_type in field_types.values():
self._arg_encoders.append(build_type_encoder(item_type, self._extra_encoders, self.type.__module__, self.force))
self._arg_encoders.append(
build_type_encoder(item_type, self._extra_encoders, self.type.__module__, self.force)
)


class TypedDictEncoder(TypeEncoder):
Expand All @@ -327,7 +329,9 @@ def __init__(self, class_name: Type, extra_encoders: TypeEncoders = None, force:
self._key_encoders = {}
self.force = force
for key_name, key_type in class_name.__annotations__.items():
self._key_encoders[key_name] = build_type_encoder(key_type, extra_encoders, class_name.__module__, self.force)
self._key_encoders[key_name] = build_type_encoder(
key_type, extra_encoders, class_name.__module__, self.force
)

def encode(self, value: dict) -> dict:
return {key: self._key_encoders[key].encode(item) for key, item in value.items()}
Expand Down
14 changes: 13 additions & 1 deletion chili/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@

from chili.error import SerialisationError

try:
from types import UnionType # type: ignore

_SUPPORT_NEW_UNION = True
except ImportError:
_SUPPORT_NEW_UNION = False
UnionType = object()


AnnotatedTypeNames = {"AnnotatedMeta", "_AnnotatedAlias"}
_GenericAlias = getattr(typing, "_GenericAlias")
_PROPERTIES = "__typed_properties__"
Expand Down Expand Up @@ -74,8 +83,11 @@ def is_serialisable(type_name: Type) -> bool:


def is_optional(type_name: Type) -> bool:
is_new_union = False
if _SUPPORT_NEW_UNION:
is_new_union = type(type_name) is UnionType
return (
get_origin_type(type_name) is Union
(get_origin_type(type_name) is Union or type(type_name) is Union or is_new_union)
and bool(get_type_args(type_name))
and get_type_args(type_name)[-1] is type(None) # noqa
)
Expand Down
270 changes: 135 additions & 135 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ license = "MIT"
name = "chili"
readme = "README.md"
repository = "https://github.com/kodemore/chili"
version = "2.8.4"
version = "2.9.0"

[tool.poetry.dependencies]
gaffe = ">=0.3.0"
Expand Down
25 changes: 24 additions & 1 deletion tests/test_decode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import sys
from dataclasses import dataclass
from enum import Enum
from typing import Generic, List, Optional, Pattern, TypeVar, Union
Expand Down Expand Up @@ -200,7 +201,7 @@ def test_decode_regex_with_flags_from_str() -> None:

# then
assert isinstance(result, Pattern)
assert result.pattern == "\d+"
assert result.pattern == "\\d+"
assert result.flags & re.I
assert result.flags & re.M
assert result.flags & re.S
Expand Down Expand Up @@ -253,3 +254,25 @@ class Pet:
# when
with pytest.raises(DecoderError.missing_property):
pet = decode(pet_data, Pet)


@pytest.mark.skipif(sys.version_info < (3, 10), reason="Unsupported python version")
def test_can_decode_new_optional_type_notation() -> None:
# given
@decodable
class Tag:
value: int | None

def __init__(self, value: int | None):
self.value = value

value = {}
alt_value = {"value": 11}

# when
result = decode(value, Tag)
alt_result = decode(alt_value, Tag)

# then
assert result.value is None
assert alt_result.value == 11
25 changes: 24 additions & 1 deletion tests/test_encode.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import datetime
import enum
import re
import sys
from collections import namedtuple
from dataclasses import dataclass
from typing import Generic, List, Optional, Pattern, Set, Tuple, TypedDict, TypeVar
from typing import Generic, List, Optional, Set, Tuple, TypedDict, TypeVar

import pytest

Expand Down Expand Up @@ -296,3 +297,25 @@ def test_can_encode_regex_with_flags_into_string() -> None:

# then
assert result == f"/{pattern_str}/imsx"


@pytest.mark.skipif(sys.version_info < (3, 10), reason="Unsupported python version")
def test_can_encode_new_optional_type_notation() -> None:
# given
@encodable
class Tag:
value: int | None

def __init__(self, value: int | None):
self.value = value

tag = Tag(None)
alt_tag = Tag(11)

# when
result = encode(tag)
alt_result = encode(alt_tag)

# then
assert result == {"value": None}
assert alt_result == {"value": 11}

0 comments on commit 6dae754

Please sign in to comment.