Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: User Generics decoders #305

Open
USSX-Hares opened this issue Jul 19, 2021 · 1 comment
Open

Feature Request: User Generics decoders #305

USSX-Hares opened this issue Jul 19, 2021 · 1 comment

Comments

@USSX-Hares
Copy link
Collaborator

USSX-Hares commented Jul 19, 2021

Hi
I would like to ask to add extended support for user-defined generics.

This means that the class should define its custom decoder and encoder which then accepts its data, generic types, field encoder/decoder (_asdict / _decode_dataclass) and **kwargs (like infer_missing).
See DecodableGenericABC in the example below.

Example Usage

Something like that

from abc import ABC, abstractmethod, ABCMeta
from dataclasses import dataclass, field
from types import GenericAlias
from typing import *
from uuid import uuid4

from dataclasses_json import DataClassJsonMixin
from dataclasses_json.core import Json

T = TypeVar('T')
K = TypeVar('K')
C = TypeVar('C')
class DecodableGenericABC(metaclass=ABCMeta):
    __slots__ = ()
    
    @abstractmethod
    def encode(self, *, data_encoder: Callable[[T], Json], **kwargs) -> Json:
        raise NotImplementedError
    
    @classmethod
    @abstractmethod
    def decode(cls: Type[C], data: Json, *types: Type[T], data_decoder: Callable[[Json], T], **kwargs) -> C:
        raise NotImplementedError
    
    __class_getitem__ = classmethod(GenericAlias)

_MISSING = object()
class CustomMapping(Mapping[K, T], Container[K], Generic[K, T], DecodableGenericABC):
    __slots__ = ('_data', '_id')
    
    _data: Dict[K, T]
    _id: str
    
    def __init__(self, data: Dict[K, T], *, id: str = None):
        self._data = dict(data)
        self._id = id or str(uuid4())
    
    def __getitem__(self, item: K) -> T:
        return self._data[item]
    
    def __len__(self) -> int:
        return len(self._data)
    
    def __iter__(self) -> Iterator[K]:
        return iter(self._data.keys())
    
    def __bool__(self) -> bool:
        return bool(self._data)
    
    def __repr__(self) -> str:
        return f'{self.__class__.__name__}(id={self._id!r}, data={self._data!r})'
    
    @property
    def id(self) -> str:
        return self._id
    
    def encode(self, *, data_encoder: Callable[[T], Json], **kwargs) -> Json:
        data_encoded = data_encoder(self._data)
        id_encoded = data_encoder(self._id)
        return dict(id=id_encoded, data=data_encoded)
    
    @classmethod
    def decode(cls, data: Json, *types, data_decoder: Callable[[Json], T], **kwargs) -> 'CustomMapping[K, T]':
        if (len(types) != 2):
            raise TypeError(f"{'Too many' if len(types) > 2 else 'Not enough'} types for decoding CustomMapping: Expected 2, got {len(types)}.")
        
        if (not isinstance(data, Mapping)):
            raise TypeError(f"'data' is expected to be Mapping, got {type(data)}")
        
        data = dict(data)
        _data = data_decoder(data.pop('data'))
        _id = data.pop('id', _MISSING)
        if (_id is not _MISSING):
            _id = data_decoder(_id)
        else:
            _id = None
        
        if (data):
            raise ValueError(f"Got unexpected fields while decoding CustomMapping: {list(data.keys())}")
        
        return cls(data=_data, id=_id)


class OptionContainer(Collection[T], Generic[T], DecodableGenericABC):
    __slots__ = ('_is_empty', '_data')
    
    _is_empty: bool
    _data: T
    
    def __init__(self, data: Optional[T]):
        self._data = data
        self._is_empty = data is None
    
    @classmethod
    def init_non_empty(cls, data: T) -> 'OptionContainer[T]':
        r = cls(data)
        r._is_empty = False
        return r
    
    @classmethod
    def init_empty(cls) -> 'OptionContainer[T]':
        r = cls(None)
        r._is_empty = True
        return r
    
    def __contains__(self, item: T) -> bool:
        return not self._is_empty and item == self._data
    
    def __iter__(self) -> Iterator[T]:
        if (not self._is_empty):
            yield self._data
    
    def __len__(self) -> int:
        return int(self._is_empty)
    
    def __bool__(self) -> bool:
        return self._is_empty
    
    def __repr__(self) -> str:
        if (self._is_empty):
            return f'{self.__class__.__name__}<empty>'
        else:
            return f'{self.__class__.__name__}({self._data!r})'
    
    def encode(self, *, data_encoder: Callable[[T], Json], **kwargs) -> Json:
        if (self._is_empty):
            return None
        else:
            return data_encoder(self._data)
    
    @classmethod
    def decode(cls, data: Json, *types: Type[T], data_decoder: Callable[[Json], T], **kwargs) -> 'OptionContainer[T]':
        if (len(types) != 1):
            raise TypeError(f"{'Too many' if len(types) > 1 else 'Not enough'} types for decoding CustomMapping: Expected 2, got {len(types)}.")
        
        if (data is None):
            return OptionContainer.init_empty()
        else:
            return OptionContainer.init_non_empty(data_decoder(data))

@dataclass
class MyClass(DataClassJsonMixin):
    opt_int: OptionContainer[int]
    opt_str: OptionContainer[str]
    opt_mapping: OptionContainer[CustomMapping[str, int]]
def main():
    opt_a = OptionContainer.init_non_empty(124)
    opt_b = OptionContainer.init_empty()
    opt_c = OptionContainer(CustomMapping(dict(f1=1, f2=2), id='135121-231566677'))
    
    my_cls = MyClass(opt_a, opt_b, opt_c)
    print(my_cls)
    
    actual_json = my_cls.to_json()
    print(actual_json)
    # Actual: {"opt_int": [124], "opt_str": [], "opt_mapping": [{"f1": 1, "f2": 2}]}
    # Wanted: {"opt_int": 124, "opt_str": null, "opt_mapping": {"id": "135121-231566677", "data": {"f1": 1, "f2": 2}}}
    
    wanted_json = '''{"opt_int": 124, "opt_str": null, "opt_mapping": {"id": "135121-231566677", "data": {"f1": 1, "f2": 2}}}'''
    decoded = my_cls.from_json(wanted_json)
    print(decoded)
    # Actual: MyClass(opt_int=OptionContainer(124), opt_str=None, opt_mapping=OptionContainer(<generator object _decode_items.<locals>.<genexpr> at 0x000002380CB59F90>))
    # Wanted: MyClass(opt_int=OptionContainer(124), opt_str=OptionContainer<empty>, opt_mapping=OptionContainer(CustomMapping(id='135121-231566677', data={'f1': 1, 'f2': 2})))
    
    return 0

if (__name__ == '__main__'):
    exit_code = main()
    exit(exit_code)
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Jul 23, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Jul 23, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Jul 23, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Jul 23, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
USSX-Hares added a commit to USSX-Hares/dataclasses-json that referenced this issue Sep 29, 2021
@george-zubrienko
Copy link
Collaborator

Should be possible in proposed v1 API: #442

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants