Skip to content

Commit

Permalink
Merge pull request #35 from NextThought/variant-error
Browse files Browse the repository at this point in the history
Introduce VariantValidationError as an error raised by Variant fields.
  • Loading branch information
jamadden committed Sep 28, 2018
2 parents 69d3840 + 3f695b6 commit 53be9ba
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 34 deletions.
6 changes: 4 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
=========


1.7.1 (unreleased)
1.8.0 (unreleased)
==================

- Nothing changed yet.
- Add ``VariantValidationError``, an error raised by variant fields
when none of their constituent fields could adapt or validate the
value.


1.7.0 (2018-09-19)
Expand Down
58 changes: 30 additions & 28 deletions src/nti/schema/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@
# stdlib imports
import numbers
import re
import sys

try:
import collections.abc as abcs
except ImportError: # pragma: no cover
# Python 2
import collections as abcs

from six import reraise
from six import string_types
from six import text_type
from zope import interface
Expand Down Expand Up @@ -77,6 +75,7 @@
from nti.schema.interfaces import IFromObject
from nti.schema.interfaces import IListOrTuple
from nti.schema.interfaces import IVariant
from nti.schema.interfaces import VariantValidationError


__docformat__ = "restructuredtext en"
Expand Down Expand Up @@ -364,7 +363,7 @@ def __init__(self, fields, variant_raise_when_schema_provided=False, **kwargs):
"""
if not fields or not all((sch_interfaces.IField.providedBy(x) for x in fields)):
raise sch_interfaces.WrongType()
raise sch_interfaces.SchemaNotProvided(sch_interfaces.IField)

# assign our children first so anything we copy to them as a result of the super
# constructor (__name__) gets set
Expand Down Expand Up @@ -405,6 +404,7 @@ def bind(self, obj):

def _validate(self, value):
super(Variant, self)._validate(value)
errors = []
for field in self.fields:
try:
field.validate(value)
Expand All @@ -415,52 +415,54 @@ def _validate(self, value):
and hasattr(field, 'schema')
and field.schema.providedBy(value)):
self._reraise_validation_error(e, value)
if field is self.fields[-1]:
# The last chance raised an exception. Nothing worked,
# so bail.
self._reraise_validation_error(e, value)
# We can never get here
raise AssertionError("This code should never be reached.")
raise AssertionError("This is never reached")
else:
errors.append(e)
try:
raise VariantValidationError(self, value, errors)
finally:
# break cycles
e = errors = None

def fromObject(self, obj):
"""
Similar to `fromUnicode`, attempts to turn the given object into something
acceptable and valid for this field. Raises a TypeError, ValueError, or
schema ValidationError if this cannot be done. Adaptation is attempted in the order
in which fields were given to the constructor. Some fields cannot be used to adapt.
Similar to `fromUnicode`, attempts to turn the given object
into something acceptable and valid for this field. Raises a
`~.VariantValidationError` if this isn't possible. Adaptation
is attempted in the order in which fields were given to the
constructor. Some fields cannot be used to adapt.
.. versionchanged:: 1.8.0
Raise `~.VariantValidationError` instead of whatever
last error we got.
"""

exc_info = None
errors = []

for field in self.fields:
try:
converter = _FieldConverter(field)

# Try to convert and validate
adapted = converter(obj)
except (TypeError, sch_interfaces.ValidationError):
except (TypeError, sch_interfaces.ValidationError) as ex:
# Nope, no good
exc_info = sys.exc_info()
errors.append(ex)
else:
# We got one that like the type. Do the validation
# now, and then return. Don't try to convert with others;
# this is probably our best error
# We got one that likes the type. Do the validation
# now, and then return if successful.
try:
field.validate(adapted)
return adapted
except sch_interfaces.SchemaNotProvided: # pragma: no cover
# Except in one case. Some schema provides adapt to something
# that they do not actually want (e.g.,
# ISanitizedHTMLContent can adapt as IPlainText
# when empty)
# so ignore that and keep trying
exc_info = sys.exc_info()
except sch_interfaces.ValidationError as ex: # pragma: no cover
errors.append(ex)

# We get here if nothing worked and re-raise the last exception
try:
reraise(*exc_info)
raise VariantValidationError(self, obj, errors)
finally:
del exc_info
# break cycles
ex = errors = None

_EVENT_TYPES = (
(string_types, BeforeTextAssignedEvent),
Expand Down
22 changes: 22 additions & 0 deletions src/nti/schema/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,29 @@ class IVariant(sch_interfaces.IField, IFromObject):
"""
Similar to :class:`zope.schema.interfaces.IObject`, but
representing one of several different types.
If :meth:`fromObject` or :meth:`validate` fails, it should raise a
:class:`VariantValidationError`.
"""

class VariantValidationError(sch_interfaces.ValidationError):
"""
An error raised when a value is not suitable for any of the fields
of the variant.
The `errors` attribute is an ordered sequence of validation errors,
with one raised by each field of the variant in turn.
.. versionadded:: 1.8.0
"""

#: A sequence of validation errors
errors = ()

def __init__(self, field, value, errors):
super(VariantValidationError, self).__init__()
self.with_field_and_value(field, value)
self.errors = errors

class IListOrTuple(sch_interfaces.IList):
pass
Expand Down
25 changes: 21 additions & 4 deletions src/nti/schema/tests/test_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from nti.schema.interfaces import IBeforeSequenceAssignedEvent
from nti.schema.interfaces import IVariant
from nti.schema.interfaces import IFromObject
from nti.schema.interfaces import VariantValidationError
# Import from the BWC location

with warnings.catch_warnings():
Expand Down Expand Up @@ -139,8 +140,12 @@ def test_variant(self):

assert_that(syntax_or_lookup.fromObject(u'foo'), is_(u'foo'))

assert_that(calling(syntax_or_lookup.fromObject).with_args(object()),
raises(TypeError))
with self.assertRaises(VariantValidationError) as exc:
syntax_or_lookup.fromObject(object())

assert_that(exc.exception,
has_property('errors',
has_length(len(syntax_or_lookup.fields))))

def test_getDoc(self):
syntax_or_lookup = Variant((Object(cmn_interfaces.ISyntaxError),
Expand All @@ -163,9 +168,21 @@ def test_complex_variant(self):
for d in {u'k': u'v'}, u'foo', [1, 2, 3]:
assert_that(d, validated_by(variant))

# It rejects these
# It rejects these by raising a VariantValidationError
# with the same number of errors as fields
for d in {u'k': 1}, b'foo', [1, 2, u'b']:
assert_that(d, not_validated_by(variant))
with self.assertRaises(VariantValidationError) as exc:
variant.fromObject(d)

assert_that(exc.exception, has_property("errors",
has_length(len(variant.fields))))

with self.assertRaises(VariantValidationError) as exc:
variant.validate(d)

assert_that(exc.exception, has_property("errors",
has_length(len(variant.fields))))

# A name set now is reflected down the line
variant.__name__ = 'baz'
Expand Down Expand Up @@ -217,7 +234,7 @@ def validate(self, value):

def test_invalid_construct(self):
assert_that(calling(Variant).with_args(()),
raises(WrongType))
raises(SchemaNotProvided))

class TestConfiguredVariant(unittest.TestCase):

Expand Down

0 comments on commit 53be9ba

Please sign in to comment.