Skip to content

Commit

Permalink
Constructor for SumType
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Avanov committed Jun 8, 2019
1 parent 391f6e7 commit a3d717a
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 71 deletions.
68 changes: 56 additions & 12 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typeit import parser as p
from typeit import flags
from typeit import schema
from typeit.sums import SumType
from typeit.sums import SumType, Either


def test_parser_empty_struct():
Expand Down Expand Up @@ -222,26 +222,71 @@ class Enums(Enum):
A = 'a'
B = 'b'

class Sums(SumType):
class A(str): ...

class B(str): ...

class X(NamedTuple):
e: Enums
s: Sums

mk_x, dict_x = p.type_constructor(X)

data = {'e': 'a', 's': 'b'}
data = {'e': 'a'}
x = mk_x(data)
assert isinstance(x.e, Enums)
assert isinstance(x.s, Sums)
assert isinstance(x.s('value'), Sums)
assert data == dict_x(x)

with pytest.raises(typeit.Invalid):
x = mk_x({'e': 'a', 's': None})
x = mk_x({'e': None})


def test_sum_types_as_union():
class Data(NamedTuple):
value: str

class MyEither(Either):
class Left:
err: str

class Right:
data: Data
version: str
name: str

class X(NamedTuple):
x: MyEither

mk_x, dict_x = p.type_constructor ^ X
x_data = {
'x': ('left', {'err': 'Error'})
}
x = mk_x(x_data)
assert isinstance(x.x, Either)
assert isinstance(x.x, MyEither)
assert isinstance(x.x, MyEither.Left)
assert not isinstance(x.x, Either.Left)
assert not isinstance(x.x, Either.Right)
assert not isinstance(x.x, MyEither.Right)
assert isinstance(x.x.err, str)
assert x.x.err == 'Error'
assert dict_x(x) == x_data

x_data = {
'x': ('right', {
'data': {'value': 'Value'},
'version': '1',
'name': 'Name',
})
}
x = mk_x(x_data)
assert isinstance(x.x, Either)
assert isinstance(x.x, MyEither)
assert isinstance(x.x, MyEither.Right)
assert not isinstance(x.x, Either.Right)
assert not isinstance(x.x, Either.Left)
assert not isinstance(x.x, MyEither.Left)
assert isinstance(x.x.data, Data)
assert isinstance(x.x.version, str)
assert x.x.data == Data(value='Value')
assert x.x.version == '1'
assert x.x.name == 'Name'
assert dict_x(x) == x_data


def test_enum_unions_serialization():
Expand Down Expand Up @@ -379,7 +424,6 @@ class X(NamedTuple):
assert dict_x(x) == data



def test_extending():
class X(NamedTuple):
x: Money
Expand Down
48 changes: 46 additions & 2 deletions tests/test_sums.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Dict

import pytest
import pickle
from typeit.sums import SumType
Expand Down Expand Up @@ -73,13 +75,55 @@ class C: ...
assert y.y == 2
assert isinstance(y.z, float)

assert c.data is None


def test_sum_variant_subclass_positional():
class X(SumType):
class A(str): ...

B: str

x = X.A(5)
assert type(x) is X
assert isinstance(x, X)
assert isinstance(x, X.A)


def test_generic_either():
class Either(SumType):
class Left: ...

class Right: ...

# User-defined Sums should adhere base Sum
with pytest.raises(TypeError):
class BrokenEither(Either):
class Left: ...


class ServiceResponse(Either):
class Left:
errmsg: str

class Right:
payload: Dict

x = ServiceResponse.Left(errmsg='Error')
y = ServiceResponse.Right(payload={'success': True})
assert type(x) is ServiceResponse
assert isinstance(x, ServiceResponse)
assert isinstance(x, ServiceResponse.Left)
assert isinstance(x, Either)
assert not isinstance(x, Either.Left)

class AlternativeEither(SumType):
class Left: ...

class Right: ...

assert not isinstance(x, AlternativeEither)
assert not isinstance(x, int)

assert x.errmsg == 'Error'
assert y.payload == {'success': True}


57 changes: 37 additions & 20 deletions typeit/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from . import compat
from . import flags
from . import schema
from . import sums
from .schema.meta import TypeExtension
from . import interface as iface

Expand Down Expand Up @@ -136,6 +137,25 @@ def _maybe_node_for_union(
return None


def _maybe_node_for_sum_type(
typ: Type[iface.IType],
overrides: OverridesT,
supported_type=frozenset({}),
supported_origin=frozenset({})
) -> Optional[schema.nodes.SchemaNode]:
if issubclass(typ, sums.SumType):
# represents a 2-tuple of (type_from_signature, associated_schema_node)
variant_nodes: List[Tuple[Type, schema.nodes.SchemaNode]] = []
for variant in typ:
node = decide_node_type(variant.__variant_meta__.constructor, overrides)
variant_nodes.append((variant, node))
sum_node = schema.nodes.SchemaNode(
schema.types.Sum(typ=typ, variant_nodes=variant_nodes)
)
return sum_node
return None


def _maybe_node_for_literal(
typ: Type[iface.IType],
overrides: OverridesT,
Expand Down Expand Up @@ -333,22 +353,21 @@ def _maybe_node_for_overridden(
return None


PARSING_ORDER = [
_maybe_node_for_overridden,
_maybe_node_for_primitive,
_maybe_node_for_type_var,
_maybe_node_for_union,
_maybe_node_for_list,
_maybe_node_for_tuple,
_maybe_node_for_dict,
_maybe_node_for_set,
_maybe_node_for_literal,
_maybe_node_for_subclass_based,
# at this point it could be a user-defined type,
# so the parser may do another recursive iteration
# through the same plan
_node_for_type,
]
PARSING_ORDER = [ _maybe_node_for_overridden
, _maybe_node_for_primitive
, _maybe_node_for_type_var
, _maybe_node_for_union
, _maybe_node_for_list
, _maybe_node_for_tuple
, _maybe_node_for_dict
, _maybe_node_for_set
, _maybe_node_for_literal
, _maybe_node_for_sum_type
, _maybe_node_for_subclass_based
# at this point it could be a user-defined type,
# so the parser may do another recursive iteration
# through the same plan
, _node_for_type ]


def decide_node_type(
Expand Down Expand Up @@ -376,10 +395,8 @@ def decide_node_type(
)


TypeTools = Tuple[
Callable[[Dict[str, Any]], T],
Callable[[T], Union[List, Dict]]
]
TypeTools = Tuple[ Callable[[Dict[str, Any]], T]
, Callable[[T], Union[List, Dict]] ]


class _TypeConstructor:
Expand Down
76 changes: 73 additions & 3 deletions typeit/schema/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pyrsistent import pmap

from .errors import Invalid
from ..sums import SumType
from .. import sums
from ..definitions import OverridesT
from .. import interface as iface
from ..compat import PY_VERSION
Expand Down Expand Up @@ -88,7 +88,74 @@ class Tuple(col.Tuple, metaclass=meta.SubscriptableSchemaTypeM):
pass


EnumLike = t.Union[std_enum.Enum, SumType]
class Sum(SchemaType, metaclass=meta.SubscriptableSchemaTypeM):
def __init__(
self,
typ: sums.SumType,
variant_nodes: t.Sequence[
t.Tuple[
t.Type, t.Union[nodes.SchemaNode, col.SequenceSchema, col.TupleSchema]
],
],
) -> None:
super().__init__()
self.typ = typ
self.variant_nodes = variant_nodes
self.variant_schema_types: t.Set[col.SchemaType] = {
x.typ for _, x in variant_nodes
}

def deserialize(self, node, cstruct):
if cstruct in (col.null, None):
# explicitly passed None is not col.null
# therefore we must handle both
return cstruct

try:
tag, payload = cstruct
except ValueError:
raise Invalid(
node,
'Incorrect data layout for this type.',
cstruct
)
# next, iterate over available variants and return the first
# matched structure.
for var_type, var_schema in self.variant_nodes:
if var_type.__variant_meta__.value != tag:
continue
try:
variant_struct = var_schema.deserialize(payload)
except Invalid:
raise Invalid(
node,
'Incorrect payload format.',
cstruct
)
return var_type(**variant_struct._asdict())

raise Invalid(
node,
'None of the variants matches provided data.',
cstruct
)

def serialize(self, node, appstruct: t.Any):
if appstruct in (col.null, None):
return None

for var_type, var_schema in self.variant_nodes:
if isinstance(appstruct, var_type):
return (var_type.__variant_meta__.value, var_schema.serialize(appstruct))

raise Invalid(
node,
'None of the variants matches provided structure.',
appstruct
)


EnumLike = std_enum.Enum


class Enum(primitives.Str):
Expand All @@ -112,6 +179,9 @@ def deserialize(self, node, cstruct) -> std_enum.Enum:
raise Invalid(node, f'Invalid variant of {self.typ.__name__}', cstruct)





generic_type_bases: t.Callable[[t.Type], t.Tuple[t.Type, ...]] = (
insp.get_generic_bases if PY_VERSION < (3, 7) else
lambda x: (insp.get_origin(x),)
Expand Down Expand Up @@ -285,7 +355,7 @@ def serialize(self, node, appstruct: t.Any):
SUBCLASS_BASED_TO_SCHEMA_TYPE: t.Mapping[
t.Tuple[t.Type, ...], t.Type[SchemaType],
] = {
(std_enum.Enum, SumType): Enum,
(std_enum.Enum,): Enum,
# Pathlib's PurePath and its derivatives
(pathlib.PurePath,): Path,
}
4 changes: 4 additions & 0 deletions typeit/sums/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .impl import SumType
from .types import Either, Maybe

__all__ = ('SumType', 'Either', 'Maybe')
Loading

0 comments on commit a3d717a

Please sign in to comment.