Skip to content

Commit

Permalink
Add error handling docs section
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim Avanov committed Jul 3, 2019
1 parent 521b719 commit d7fcc22
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 73 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -6,6 +6,7 @@ __pycache__/
.idea/
.cache/
.pytest_cache/
.DS_Store

build/
dist/
Expand Down
45 changes: 44 additions & 1 deletion docs/quickstart_guide.rst
Expand Up @@ -309,4 +309,47 @@ TODO
Handling errors
---------------

TODO
Below is a quick example of how value parsing errors can be handled:

.. code-block:: python
from enum import Enum
from typing import NamedTuple, Sequence
import typeit
class ItemType(Enum):
ONE = 'one'
TWO = 'two'
class Item(NamedTuple):
val: ItemType
class X(NamedTuple):
items: Sequence[Item]
item: Item
mk_x, serialize_x = typeit.type_constructor ^ X
invalid_data = {
'items': [
{'val': 'one'},
{'val': 'two'},
{'val': 'three'},
{'val': 'four'},
]
}
try:
x = mk_x(invalid_data)
except typeit.Error as err:
for e in err:
print(f'Invalid data for `{e.path}`: {e.reason}: {repr(e.sample)} was passed')
.. code-block::
Invalid data for `items.2.val`: Invalid variant of ItemType: 'three' was passed
Invalid data for `items.3.val`: Invalid variant of ItemType: 'four' was passed
Invalid data for `item`: Required: None was passed
22 changes: 11 additions & 11 deletions tests/parser/test_parser.py
Expand Up @@ -47,10 +47,10 @@ class X(NamedTuple):
d=1,
)

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
mk_x(data)

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
serialize_x(data_X)

assert mk_x_nonstrict(data) == X(
Expand Down Expand Up @@ -135,22 +135,22 @@ class X(NamedTuple):
assert x.b == ()
assert x.b == x.c

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
# 'abc' is not int
x = mk_x({
'a': ['value', 'abc'],
'b': [],
'c': [],
})

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
# .c field is required
x = mk_x({
'a': ['value', 5],
'b': [],
})

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
# .c field is required to be fixed sequence
x = mk_x({
'a': ['value', 'abc'],
Expand Down Expand Up @@ -212,7 +212,7 @@ class X(NamedTuple):
assert isinstance(x.e, Enums)
assert data == serialize_x(x)

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


Expand Down Expand Up @@ -268,7 +268,7 @@ class X(NamedTuple):
assert x.x.name == 'Name'
assert serialize_x(x) == x_data

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
# version is missing
x = mk_x({
'x': ('right', {
Expand Down Expand Up @@ -314,7 +314,7 @@ class X(NamedTuple):
x = mk_x({'x': 1, 'y': variant.value})
assert x.y is variant

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
x = mk_x({'x': 1, 'y': None})


Expand Down Expand Up @@ -401,10 +401,10 @@ class X(NamedTuple):

mk_x, serializer = p.type_constructor(X)

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
mk_x({})

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
mk_x({'x': 1})

x = mk_x({'x': 1, 'y': {'x': 1}})
Expand All @@ -417,7 +417,7 @@ class X(NamedTuple):

data = {'my-x': 1}

with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
mk_x, serialize_x = p.type_constructor ^ X
mk_x(data)

Expand Down
8 changes: 4 additions & 4 deletions tests/std_types/test_typing.py
Expand Up @@ -4,7 +4,7 @@
from typing_extensions import Literal

import pytest
from typeit import type_constructor, Invalid
from typeit import type_constructor, Error


def test_mapping():
Expand Down Expand Up @@ -69,11 +69,11 @@ class X(NamedTuple):
'y': 'a',
},
):
with pytest.raises(Invalid):
with pytest.raises(Error):
mk_x(case)

x = X(None, None, 3)
with pytest.raises(Invalid):
with pytest.raises(Error):
serialize_x(x)


Expand All @@ -100,7 +100,7 @@ class X(NamedTuple):
'z': [1, 1],
})

with pytest.raises(Invalid):
with pytest.raises(Error):
mk_x({
'x': None,
'y': None,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_pyrsistent.py
Expand Up @@ -30,5 +30,5 @@ class X(NamedTuple):
'b': [{'x': 'x', 'y': 'y'}],
'c': {'x': 'x', 'y': 'y'}
}
with pytest.raises(ty.Invalid):
with pytest.raises(ty.Error):
mk_x(data)
7 changes: 4 additions & 3 deletions tests/test_utils.py
@@ -1,4 +1,5 @@
import typeit
from typeit.schema.errors import InvalidData
from typeit import utils
from typing import NamedTuple, Sequence
from typeit import type_constructor
Expand Down Expand Up @@ -40,6 +41,6 @@ class X(NamedTuple):

try:
x = mk_x(data)
except typeit.Invalid as e:
for inv in utils.iter_invalid(e, data):
assert isinstance(inv, utils.InvalidData)
except typeit.Error as e:
for inv in e:
assert isinstance(inv, InvalidData)
3 changes: 1 addition & 2 deletions tests/unions/test_unions.py
@@ -1,6 +1,5 @@
from typing import NamedTuple, Union, Any, Dict

import colander
import pytest

import typeit
Expand Down Expand Up @@ -32,7 +31,7 @@ class X(NamedTuple):
assert data == serialize_x(x)

assert mk_x({'x': None, 'y': 'y'}) == mk_x({'y': 'y'})
with pytest.raises(typeit.Invalid):
with pytest.raises(typeit.Error):
# this is not the same as mk_x({}),
# the empty structure is passed as attribute x,
# which should match with only an empty named tuple definition,
Expand Down
8 changes: 3 additions & 5 deletions typeit/__init__.py
@@ -1,7 +1,5 @@
from .parser import type_constructor
from .codegen import typeit
from . import flags
from .schema.errors import Invalid
from .utils import iter_invalid
from .parser import type_constructor
from .schema.errors import Error

__all__ = ['type_constructor', 'typeit', 'flags', 'Invalid', 'iter_invalid']
__all__ = ('type_constructor', 'flags', 'Error')
7 changes: 6 additions & 1 deletion typeit/parser.py
@@ -1,3 +1,4 @@
from functools import partial
from typing import (
Type, Tuple, Optional, Any, Union, List, Set,
Dict, Callable,
Expand All @@ -19,6 +20,7 @@
from . import schema
from . import sums
from .schema.meta import TypeExtension
from .schema.errors import errors_aware_constructor
from . import interface as iface


Expand Down Expand Up @@ -443,7 +445,10 @@ def __call__(self,
raise TypeError(
f'Cannot create a type constructor for {typ}: {e}'
)
return schema_node.deserialize, schema_node.serialize
return (
partial(errors_aware_constructor, schema_node.deserialize),
partial(errors_aware_constructor, schema_node.serialize)
)

def __and__(self, override: OverrideT) -> '_TypeConstructor':
if isinstance(override, flags._Flag):
Expand Down
66 changes: 66 additions & 0 deletions typeit/schema/errors.py
@@ -1,5 +1,71 @@
from typing import NamedTuple, Optional, Any, Mapping, Iterator, Union, TypeVar, Callable

import colander

# Alias for typeit.Invalid, so users don't have to import two
# packages
from typeit import interface as iface

Invalid = colander.Invalid


class InvalidData(NamedTuple):
path: str
reason: str
sample: Optional[Any]


class Error(ValueError):
def __init__(self, validation_error, sample_data):
super().__init__()
self.validation_error = validation_error
self.sample_data = sample_data

def __iter__(self) -> Iterator[InvalidData]:
return iter_invalid(self.validation_error, self.sample_data)


def iter_invalid(error: Invalid,
data: Mapping[str, Any]) -> Iterator[InvalidData]:
""" A helper function to iterate over data samples that
caused an exception at object construction. Use it like this:
>>> try:
>>> obj = mk_obj(data)
>>> except Invalid as e:
>>> # iterate over a sequence of InvalidData
>>> for e in iter_invalid(e, data):
>>> ...
"""
for e_path, msg in error.asdict().items():
e_parts = []
for x in e_path.split('.'):
# x is either a list index or a dict key
try:
x = int(x)
except ValueError:
pass
e_parts.append(x)
# traverse data for a value that caused an error
traversed_value: Union[None, iface.ITraversable] = data
for i in e_parts:
try:
traversed_value = traversed_value[i]
except KeyError:
# handles the case when key is missing from payload
traversed_value = None
break
yield InvalidData(path=e_path, reason=msg, sample=traversed_value)


T = TypeVar('T')
S = TypeVar('S')


def errors_aware_constructor(construct: Callable[[T], S], data: T) -> S:
try:
return construct(data)
except Invalid as e:
raise Error(validation_error=e,
sample_data=data)
46 changes: 1 addition & 45 deletions typeit/utils.py
@@ -1,20 +1,10 @@
import re
import keyword
from typing import Iterator, NamedTuple, Any, Mapping, Union, Optional, Type

from .schema.errors import Invalid
from . import interface as iface

from typing import Any, Type

NORMALIZATION_PREFIX = 'overridden__'


class InvalidData(NamedTuple):
path: str
reason: str
sample: Optional[Any]


def normalize_name(name: str,
pattern=re.compile('^([_0-9]+).*$')) -> str:
""" Some field name patterns are not allowed in NamedTuples
Expand All @@ -26,39 +16,5 @@ def normalize_name(name: str,
return being_normalized


def iter_invalid(error: Invalid,
data: Mapping[str, Any]) -> Iterator[InvalidData]:
""" A helper function to iterate over data samples that
caused an exception at object construction. Use it like this:
>>> try:
>>> obj = mk_obj(data)
>>> except Invalid as e:
>>> # iterate over a sequence of InvalidData
>>> for e in iter_invalid(e, data):
>>> ...
"""
for e_path, msg in error.asdict().items():
e_parts = []
for x in e_path.split('.'):
# x is either a list index or a dict key
try:
x = int(x)
except ValueError:
pass
e_parts.append(x)
# traverse data for a value that caused an error
traversed_value: Union[None, iface.ITraversable] = data
for i in e_parts:
try:
traversed_value = traversed_value[i]
except KeyError:
# handles the case when key is missing from payload
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 d7fcc22

Please sign in to comment.