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

polymorfic #33

Merged
merged 12 commits into from
Jan 2, 2020
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ python:
script: nose2 -v && mypy tests dataclass_factory
install:
- pip3 install -r requirements.txt
- pip3 install nose2 mypy
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ factory.load(1, int) # prints: parsing done
* `Optional`
* `Any`, using this type no conversion is done during parsing. But serialization is based on real data type
* `Union`
* `Literal` types, including variant from `typing_exstensions`
* `dataclass`
* `Generic` dataclasses
* `datetime` and `UUID` can be converted using predefined schemas
Expand Down
23 changes: 19 additions & 4 deletions dataclass_factory/parsers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import decimal
import inspect
from collections import deque
from dataclasses import fields, is_dataclass

import itertools
from collections import deque
from typing import (
List, Set, FrozenSet, Deque, Any, Callable,
Dict, Collection, Type, get_type_hints,
Optional, Tuple, Union)
Optional, Tuple, Union, Sequence)

from dataclasses import fields, is_dataclass

from .common import Parser, T
from .exceptions import InvalidFieldError
Expand All @@ -17,6 +17,7 @@
is_tuple, is_collection, is_any, hasargs, is_optional,
is_none, is_union, is_dict, is_enum,
is_generic_concrete, fill_type_args, args_unspecified,
is_literal, is_literal36,
)

PARSER_EXCEPTIONS = (ValueError, TypeError, AttributeError, LookupError)
Expand Down Expand Up @@ -215,6 +216,16 @@ def class_parser(data):
return class_parser


def get_literal_parser(factory, values: Sequence[Any]) -> Parser:
def literal_parser(data: Any):
for v in values:
if (type(v), v) == (type(data), data):
return data
raise ValueError("Invalid literal data")

return literal_parser


def get_lazy_parser(factory, class_: Type) -> Parser:
# return partial(factory.load, class_=class_)
def lazy_parser(data):
Expand Down Expand Up @@ -245,6 +256,10 @@ def create_parser_impl(factory, schema: Schema, debug_path: bool, cls: Type) ->
return parse_stub
if is_none(cls):
return parse_none
if is_literal(cls):
return get_literal_parser(factory, cls.__args__)
if is_literal36(cls):
return get_literal_parser(factory, cls.__values__)
if is_optional(cls):
return get_optional_parser(factory.parser(cls.__args__[0]))
if cls in (str, bytearray, bytes):
Expand Down
31 changes: 30 additions & 1 deletion dataclass_factory/type_detection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import inspect
from enum import Enum

from typing import Collection, Tuple, Optional, Any, Dict, Union, Type, TypeVar, Generic
from typing import Collection, Tuple, Optional, Any, Dict, Union, Type, TypeVar, Generic, List

LITERAL_TYPES: List[Any] = []
try:
from typing import Literal as PyLiteral # type: ignore

LITERAL_TYPES.append(PyLiteral)
except ImportError:
pass

try:
CompatLiteral: Any
from typing_extensions import Literal as CompatLiteral # type: ignore

LITERAL_TYPES.append(CompatLiteral)
except ImportError:
CompatLiteral = None


def hasargs(type_, *args) -> bool:
Expand Down Expand Up @@ -78,6 +94,19 @@ def args_unspecified(cls: Type) -> bool:
)


def is_literal(cls) -> bool:
return is_generic_concrete(cls) and cls.__origin__ in LITERAL_TYPES


def is_literal36(cls) -> bool:
if not CompatLiteral:
return False
try:
return cls == CompatLiteral[cls.__values__]
except AttributeError:
return False


def is_dict(cls) -> bool:
try:
dicts = (dict, Dict)
Expand Down
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
dataclasses
dataclasses;python_version<"3.7"
nose2
mypy
typing_extensions
36 changes: 36 additions & 0 deletions tests/test_literal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import sys
from typing import Any
from unittest import TestCase

from nose2.tools import params # type: ignore
from typing_extensions import Literal as CompatLiteral

from dataclass_factory import Factory

LITERALS: Any = [CompatLiteral]
if sys.version_info >= (3, 8):
from typing import Literal as PyLiteral

LITERALS.append(PyLiteral)


class TestLiteral(TestCase):
def setUp(self) -> None:
self.factory = Factory()

@params(*LITERALS)
def test_literal_fail(self, literal):
abc = literal["a", "b", "c"]
one = literal[1]
with self.assertRaises(ValueError):
self.factory.load("d", abc)
with self.assertRaises(ValueError):
self.factory.load(1.0, one)

@params(*LITERALS)
def test_literal(self, literal):
abc = literal["a", "b", "c"]
one = literal[1]
self.assertEqual(self.factory.load("a", abc), "a")
self.assertEqual(self.factory.load("b", abc), "b")
self.assertEqual(self.factory.load(1, one), 1)