Skip to content

Commit

Permalink
Accept annotated init-based objects #23
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Avanov committed Jun 23, 2019
1 parent bf7c7f5 commit 66f6b70
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 72 deletions.
Empty file added tests/parser/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions tests/parser/test_dataclasses.py
@@ -0,0 +1,25 @@
from typeit.compat import PY_VERSION

if PY_VERSION >= (3, 7):
from dataclasses import dataclass
from typeit import parser as p


def test_dataclasses():

@dataclass
class InventoryItem:
name: str
unit_price: float
quantity_on_hand: int = 0

mk_inv, dict_inv = p.type_constructor(InventoryItem)

serialized = {
'name': 'test',
'unit_price': 1.0,
'quantity_on_hand': 5,
}
x = mk_inv(serialized)
assert isinstance(x, InventoryItem)
assert dict_inv(x) == serialized
51 changes: 51 additions & 0 deletions tests/parser/test_extending.py
@@ -0,0 +1,51 @@
from typing import NamedTuple, Optional

from money.currency import Currency
from money.money import Money

import typeit
from typeit import schema, parser as p


def test_extending():
class X(NamedTuple):
x: Money

class MoneySchema(schema.types.Tuple):
def deserialize(self, node, cstruct):
r = super().deserialize(node, cstruct)
if r in (schema.types.Null, None):
return r
try:
currency = Currency(r[0])
except ValueError:
raise typeit.Invalid(node, f'Invalid currency token in {r}', cstruct)

try:
rv = Money(r[1], currency)
except:
raise typeit.Invalid(node, f'Invalid amount in {r}', cstruct)

return rv

def serialize(self, node, appstruct: Optional[Money]):
if appstruct is None or appstruct is schema.types.Null:
# if appstruct is None or appstruct is schema.types.Null:
return appstruct

r = (appstruct.currency, appstruct.amount)
return super().serialize(node, r)

mk_x, dict_x = (
p.type_constructor
& MoneySchema[Money] << schema.types.Enum(Currency) << schema.primitives.NonStrictStr()
^ X
)

serialized = {
'x': ('GBP', '10')
}

x = mk_x(serialized)
assert isinstance(x.x, Money)
assert dict_x(x) == serialized
51 changes: 0 additions & 51 deletions tests/test_parser.py → tests/parser/test_parser.py
Expand Up @@ -3,8 +3,6 @@
from typing import NamedTuple, Dict, Any, Sequence, Union, Tuple, Optional, Set, List, FrozenSet, get_type_hints

import pytest
from money.currency import Currency
from money.money import Money

import typeit
from typeit import codegen as cg
Expand Down Expand Up @@ -432,55 +430,6 @@ class X(NamedTuple):
assert dict_x(x) == data


def test_extending():
class X(NamedTuple):
x: Money

class MoneySchema(schema.types.Tuple):
def deserialize(self, node, cstruct):
r = super().deserialize(node, cstruct)
if r in (schema.types.Null, None):
return r
try:
currency = Currency(r[0])
except ValueError:
raise typeit.Invalid(node, f'Invalid currency token in {r}', cstruct)

try:
rv = Money(r[1], currency)
except:
raise typeit.Invalid(node, f'Invalid amount in {r}', cstruct)

return rv

def serialize(self, node, appstruct: Optional[Money]):
if appstruct is None or appstruct is schema.types.Null:
# if appstruct is None or appstruct is schema.types.Null:
return appstruct

r = (appstruct.currency, appstruct.amount)
return super().serialize(node, r)

with pytest.raises(TypeError):
# type ``Money`` is not defined in overrides
__ = p.type_constructor ^ X

mk_x, dict_x = (
p.type_constructor
& MoneySchema[Money] << schema.types.Enum(Currency) << schema.primitives.NonStrictStr()
^ X
)

serialized = {
'x': ('GBP', '10')
}

x = mk_x(serialized)
assert isinstance(x.x, Money)
assert dict_x(x) == serialized



GITHUB_PR_PAYLOAD_JSON = """
{
"action": "closed",
Expand Down
18 changes: 18 additions & 0 deletions tests/parser/test_regular_classes.py
@@ -0,0 +1,18 @@
from money.money import Money

from typeit import parser as p
from typeit import flags


def test_regular_classes():

mk_x, dict_x = p.type_constructor & flags.NON_STRICT_PRIMITIVES ^ Money

serialized = {
'amount': '10',
'currency': 'GBP',
}

x = mk_x(serialized)
assert isinstance(x, Money)
assert dict_x(x) == serialized
45 changes: 37 additions & 8 deletions typeit/parser.py
Expand Up @@ -9,10 +9,11 @@

import colander as col
import typing_inspect as insp
from pyrsistent import pmap
from pyrsistent import pmap, pvector
from pyrsistent import typing as pyt

from .definitions import OverridesT, NO_OVERRIDES
from .utils import is_named_tuple
from . import compat
from . import flags
from . import schema
Expand All @@ -23,6 +24,8 @@

T = TypeVar('T')

NoneType = type(None)


OverrideT = Union[
# flag override
Expand Down Expand Up @@ -165,7 +168,7 @@ def _maybe_node_for_literal(
insp.get_generic_type(Literal) # py3.6 fix
}),
_supported_literal_types=frozenset({
bool, int, str, bytes, type(None),
bool, int, str, bytes, NoneType,
})
) -> Optional[schema.nodes.SchemaNode]:
""" Handles cases where typ is a Literal, according to the allowed
Expand Down Expand Up @@ -323,14 +326,40 @@ def _node_for_type(
if type(typ) is not type:
return None

if not hasattr(typ, '_fields'):
return None
if is_named_tuple(typ):
deserialize_overrides = pmap({
overrides[getattr(typ, x)]: x
for x in typ._fields
if getattr(typ, x) in overrides
})
hints_source = typ
else:
# use init-based types;
# note that overrides are not going to work without named tuples
deserialize_overrides = pmap({})
hints_source = typ.__init__

attribute_hints = list(filter(
lambda x: x[1] is not NoneType,
get_type_hints(hints_source).items()
))

type_schema = schema.nodes.SchemaNode(
schema.types.Structure(
typ=typ,
overrides=overrides,
attrs=pvector([x[0] for x in attribute_hints]),
deserialize_overrides=deserialize_overrides,
)
)

type_schema = schema.nodes.SchemaNode(schema.types.Structure(typ, overrides))
for field_name, field_type in get_type_hints(typ).items():
for field_name, field_type in attribute_hints:
# apply field override, if available
field = getattr(typ, field_name)
serialized_field_name = overrides.get(field, field_name)
if deserialize_overrides:
field = getattr(typ, field_name)
serialized_field_name = overrides.get(field, field_name)
else:
serialized_field_name = field_name

node_type = decide_node_type(field_type, overrides)
if node_type is None:
Expand Down
6 changes: 4 additions & 2 deletions typeit/schema/primitives.py
Expand Up @@ -14,7 +14,8 @@ def _strict_deserialize(node, rval, cstruct):
if type(rval) is not type(cstruct):
raise Invalid(
node,
'Primitive values should adhere strict type semantics',
f'Primitive values should adhere strict type semantics: '
f'{type(rval)} was passed, {type(cstruct)} is expected by deserializer.',
cstruct
)
return rval
Expand All @@ -27,7 +28,8 @@ def _strict_serialize(node, allowed_type, appstruct):
if type(appstruct) is not allowed_type:
raise Invalid(
node,
'Primitive values should adhere strict type semantics',
f'Primitive values should adhere strict type semantics: '
f'{type(appstruct)} was passed, {allowed_type} is expected by serializer.',
appstruct
)
return appstruct
Expand Down
23 changes: 13 additions & 10 deletions typeit/schema/types.py
Expand Up @@ -4,7 +4,8 @@

import typing_inspect as insp
import colander as col
from pyrsistent import pmap
from pyrsistent import pmap, pvector
from pyrsistent.typing import PMap

from .errors import Invalid
from .. import sums
Expand Down Expand Up @@ -47,15 +48,17 @@ class Structure(col.Mapping):
def __init__(self,
typ: t.Type[iface.IType],
overrides: OverridesT,
unknown: str = 'ignore') -> None:
attrs: t.Sequence[str] = pvector([]),
deserialize_overrides: PMap[str, str] = pmap({}),
unknown: str = 'ignore',
) -> None:
"""
:param deserialize_overrides: source_field_name => struct_field_name mapping
"""
super().__init__(unknown)
self.typ = typ
# source_field_name => struct_field_name
self.deserialize_overrides = pmap({
overrides[getattr(typ, x)]: x
for x in typ._fields
if getattr(typ, x) in overrides
})
self.attrs = attrs
self.deserialize_overrides = deserialize_overrides
# struct_field_name => source_field_name
self.serialize_overrides = pmap({
v: k for k, v in self.deserialize_overrides.items()
Expand All @@ -79,8 +82,8 @@ def serialize(self, node, appstruct: iface.IType):
return super().serialize(
node,
{
self.serialize_overrides.get(k, k): v
for k, v in appstruct._asdict().items()
self.serialize_overrides.get(attr_name, attr_name): getattr(appstruct, attr_name)
for attr_name in self.attrs
}
)

Expand Down
6 changes: 5 additions & 1 deletion typeit/utils.py
@@ -1,6 +1,6 @@
import re
import keyword
from typing import Iterator, NamedTuple, Any, Mapping, Union, Optional
from typing import Iterator, NamedTuple, Any, Mapping, Union, Optional, Type

from .schema.errors import Invalid
from . import interface as iface
Expand Down Expand Up @@ -58,3 +58,7 @@ def iter_invalid(error: Invalid,
traversed_value = None
break
yield InvalidData(path=e_path, reason=msg, sample=traversed_value)


def is_named_tuple(typ: Type[Any]) -> bool:
return hasattr(typ, '_fields')

0 comments on commit 66f6b70

Please sign in to comment.