Skip to content

Commit

Permalink
Support for collections.UserString, accept any in Encoder, Decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
dkraczkowski committed Oct 30, 2023
1 parent 7948f92 commit 5191bab
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.pytest_cache
__pycache__
.DS_Store
/dist
6 changes: 5 additions & 1 deletion chili/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
is_newtype,
is_optional,
is_typed_dict,
is_user_string,
map_generic_type,
resolve_forward_reference,
unpack_optional,
Expand Down Expand Up @@ -470,6 +471,9 @@ def build_type_decoder(a_type: Type, extra_decoders: TypeDecoders = None, module
if is_class(origin_type) and is_typed_dict(origin_type):
return TypedDictDecoder(origin_type, extra_decoders)

if is_class(origin_type) and is_user_string(origin_type):
return ProxyDecoder[origin_type](origin_type)

if origin_type is Union:
type_args = get_type_args(a_type)
if len(type_args) == 2 and type_args[-1] is type(None): # type: ignore
Expand Down Expand Up @@ -568,7 +572,7 @@ def __class_getitem__(cls, item: Type) -> Type[Decoder]: # noqa: E501
item = decodable(item)

if not hasattr(item, _DECODABLE):
raise DecoderError.invalid_type
item = decodable(item)

return type( # type: ignore
f"{cls.__qualname__}[{item.__module__}.{item.__qualname__}]",
Expand Down
6 changes: 5 additions & 1 deletion chili/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
is_newtype,
is_optional,
is_typed_dict,
is_user_string,
map_generic_type,
resolve_forward_reference,
unpack_optional,
Expand Down Expand Up @@ -379,6 +380,9 @@ def build_type_encoder(a_type: Type, extra_encoders: TypeEncoders = None, module
if is_class(origin_type) and is_typed_dict(origin_type):
return TypedDictEncoder(origin_type, extra_encoders)

if is_class(origin_type) and is_user_string(origin_type):
return ProxyEncoder[str](str)

if origin_type is Union:
type_args = get_type_args(a_type)
if len(type_args) == 2 and type_args[-1] is type(None):
Expand Down Expand Up @@ -476,7 +480,7 @@ def __class_getitem__(cls, item: Any) -> Type[Encoder]: # noqa: E501
item = encodable(item)

if not hasattr(item, _ENCODABLE):
raise EncoderError.invalid_type
item = encodable(item)

return type( # type: ignore
f"{cls.__qualname__}[{item.__module__}.{item.__qualname__}]",
Expand Down
4 changes: 4 additions & 0 deletions chili/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import sys
import typing
from collections import UserString
from dataclasses import MISSING, Field, InitVar, is_dataclass
from enum import Enum
from inspect import isclass as is_class
Expand Down Expand Up @@ -113,6 +114,9 @@ def is_typed_dict(type_name: Type) -> bool:
return issubclass(type_name, dict) and hasattr(type_name, "__annotations__")


def is_user_string(type_name: Type) -> bool:
return issubclass(type_name, UserString)

def map_generic_type(type_name: Any, type_map: Dict[Any, Any]) -> Any:
if not type_map:
return type_name
Expand Down
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.4.2"
version = "2.5.0"

[tool.poetry.dependencies]
gaffe = "^0.2.1"
Expand Down
42 changes: 39 additions & 3 deletions tests/test_encoder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import UserString

import pytest

from chili import Encoder, encodable
Expand All @@ -18,12 +20,46 @@ class Example:
assert instance.__generic__ == Example


def test_fail_encode_non_encodable_type() -> None:
def test_can_encode_non_encodable_type() -> None:
# given
class Example:
name: str
age: int

def __init__(self, name: str, age: int):
self.name = name
self.age = age

# when
with pytest.raises(EncoderError.invalid_type):
Encoder[Example]()
encoder = Encoder[Example]()
value = encoder.encode(Example("bob", 33))

# then
assert value == {
"name": "bob",
"age": 33,
}


def test_can_encode_complex_non_encodable_type() -> None:
# given
class ExampleName(UserString):
pass

class Example:
name: ExampleName
age: int

def __init__(self, name: ExampleName, age: int):
self.name = name
self.age = age

# when
encoder = Encoder[Example]()
value = encoder.encode(Example(ExampleName("bob"), 33))

# then
assert value == {
"name": "bob",
"age": 33,
}
32 changes: 32 additions & 0 deletions tests/usecases/userstring_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from collections import UserString

from chili import decode, encode


def test_can_encode_userstring() -> None:
# given
class ComplexString(UserString):
pass

string = ComplexString("Example String")

# when
result = encode(string)

# then
assert result == "Example String"


def test_can_decode_userstring() -> None:
# given
class ComplexString(UserString):
pass
string = "Example String"

# when
result = decode(string, ComplexString)

# then
assert result == ComplexString("Example String")
assert isinstance(result, ComplexString)
assert isinstance(result, UserString)

0 comments on commit 5191bab

Please sign in to comment.