Skip to content

Commit

Permalink
100% coverage for validate_field_value.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Sep 21, 2017
1 parent b2cb2da commit d1ea6e0
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -10,3 +10,4 @@
- Add support for Python 3.
- Drop support for externalizing to plists. See
https://github.com/NextThought/nti.externalization/issues/21
- Reach 100% test coverage and ensure we remain there through CI.
23 changes: 13 additions & 10 deletions src/nti/externalization/internalization.py
Expand Up @@ -265,7 +265,7 @@ def _object_hook(k, v, x):
def _recall(k, obj, ext_obj, kwargs):
obj = update_from_external_object(obj, ext_obj, **kwargs)
obj = kwargs['object_hook'](k, obj, ext_obj)
if IPersistent.providedBy(obj):
if IPersistent.providedBy(obj): # pragma: no cover
obj._v_updated_from_external_source = ext_obj
return obj

Expand Down Expand Up @@ -297,13 +297,14 @@ def notifyModified(containedObject, externalObject, updater=None, external_keys=
# Let the updater have its shot at modifying the event, too, adding
# interfaces or attributes. (Note: this was added to be able to provide
# sharedWith information on the event, since that makes for a better stream.
# If that use case expands, revisit this interface
# If that use case expands, revisit this interface.
# XXX: Document and test this.
try:
meth = updater._ext_adjust_modified_event
except AttributeError:
pass
else:
event = meth(event)
event = meth(event) # pragma: no cover
_zope_event_notify(event)
return event

Expand Down Expand Up @@ -506,17 +507,20 @@ def validate_field_value(self, field_name, field, value):
# Like SchemaNotProvided, but for a primitive type,
# most commonly a date
# Can we adapt?
if len(e.args) != 3:
if len(e.args) != 3: # pragma: no cover
raise
exc_info = sys.exc_info()
exp_type = e.args[1]
# If the type unambiguously implements an interface (one interface)
# that's our target. IDate does this
if len(list(interface.implementedBy(exp_type))) != 1:
raise
try:
raise
finally:
del exc_info
schema = list(interface.implementedBy(exp_type))[0]
try:
value = component.getAdapter(value, schema)
value = schema(value)
except (LookupError, TypeError):
# No registered adapter, darn
raise reraise(*exc_info)
Expand All @@ -540,10 +544,9 @@ def validate_field_value(self, field_name, field, value):
# if the error is one that may be solved via simple adaptation
# TODO: This is also thrown from IObject fields when validating the
# fields of the object
exc_info = sys.exc_info()
if not e.args or not all((isinstance(x, SchemaNotProvided) for x in e.args[0])):
raise

raise # pragma: no cover
exc_info = sys.exc_info()
# IObject provides `schema`, which is an interface, so we can adapt
# using it. Some other things do not, for example nti.schema.field.Variant
# They might provide a `fromObject` function to do the conversion
Expand Down Expand Up @@ -584,7 +587,7 @@ def validate_field_value(self, field_name, field, value):
del exc_info

if (field.readonly
and field.get(self) is None
and field.query(self) is None
and field.queryTaggedValue('_ext_allow_initial_set')):
if value is not None:
# First time through we get to set it, but we must bypass
Expand Down
176 changes: 176 additions & 0 deletions src/nti/externalization/tests/test_internalization.py
Expand Up @@ -24,6 +24,7 @@

from hamcrest import assert_that
from hamcrest import calling
from hamcrest import contains
from hamcrest import equal_to
from hamcrest import has_entry
from hamcrest import has_length
Expand Down Expand Up @@ -401,3 +402,178 @@ def __call__(self):
self._callFUT(None, ext_wrapper)

assert_that(ext_wrapper, has_entry('a', is_(TrivialFactory)))


class TestValidateFieldValue(CleanUp,
unittest.TestCase):

class Bag(object):
pass

def _callFUT(self, field, value, bag=None):
bag = bag or self.Bag()
setter = INT.validate_field_value(bag, field.__name__, field, value)
return setter, bag

def test_schema_not_provided_adapts(self):
from zope.schema import Object

class IThing(interface.Interface):
pass

field = Object(IThing, __name__='field')

class O(object):

def __conform__(self, iface):
assert iface is IThing
interface.alsoProvides(self, iface)
return self

setter, bag = self._callFUT(field, O())

setter()
assert_that(bag, has_property('field', is_(O)))

def test_wrong_type_adapts(self):
from zope.schema import Field
from zope.schema.interfaces import WrongType
from zope.schema.interfaces import ValidationError

class Iface(interface.Interface):
pass
@interface.implementer(Iface)
class TheType(object):
pass

class MyField(Field):
_type = TheType

class MyObject(object):
pass

field = MyField(__name__='field')

with self.assertRaises(WrongType):
self._callFUT(field, MyObject())

class MyConformingObject(object):
def __conform__(self, iface):
assert iface is Iface
interface.alsoProvides(self, iface)
# Note that we have to return this exact type, other wise
# we get stuck in an infinite loop.
return TheType()

setter, bag = self._callFUT(field, MyConformingObject())
setter()
assert_that(bag, has_property('field', is_(TheType)))

class MyInvalidObject(object):
def __conform__(self, iface):
raise ValidationError()

with self.assertRaises(ValidationError) as exc:
self._callFUT(field, MyInvalidObject())

assert_that(exc.exception, has_property('field', field))

def test_wrong_contained_type_object_field_adapts(self):
from zope.schema import Object
from zope.schema import List
from zope.schema.interfaces import WrongContainedType

class IThing(interface.Interface):
pass

field = List(value_type=Object(IThing), __name__='field')

class O(object):
def __conform__(self, iface):
assert iface is IThing, iface
interface.alsoProvides(self, iface)
return self

setter, bag = self._callFUT(field, [O()])

setter()
assert_that(bag, has_property('field', contains(is_(O))))

class N(object):
def __conform__(self, iface):
raise TypeError()

with self.assertRaises(WrongContainedType):
self._callFUT(field, [N()])

def test_wrong_contained_type_field_fromObject(self):
from zope.schema import Object
from zope.schema import List
from zope.schema.interfaces import WrongContainedType

class FromList(List):
def fromObject(self, o):
assert isinstance(o, list)
return o

class IThing(interface.Interface):
pass

field = FromList(value_type=Object(IThing))

# This gets us to the second pass, after we run the fromObject
# one time.
with self.assertRaises(WrongContainedType):
self._callFUT(field, [object()])

def test_wrong_contained_type_value_type_fromObject(self):
from zope.schema import Object
from zope.schema import List

class IThing(interface.Interface):
pass


class FromObject(Object):

def fromObject(self, o):
interface.alsoProvides(o, IThing)
return o

class O(object):
pass

field = List(value_type=FromObject(IThing), __name__='field')

setter, bag = self._callFUT(field, [O()])
setter()
assert_that(bag, has_property('field'))


def test_readonly_allowed(self):
from zope.schema import Int

field = Int(readonly=True, __name__='field', required=False)
field.setTaggedValue('_ext_allow_initial_set', True)

setter, bag = self._callFUT(field, 1)
setter()
assert_that(bag, has_property('field', 1))

# Second time it is not allowed
setter, bag = self._callFUT(field, 2, bag=bag)
with self.assertRaises(TypeError):
setter()
assert_that(bag, has_property('field', 1))

# If we send none, it is bypassed
setter, bag = self._callFUT(field, None)
setter()
assert_that(bag, is_not(has_property('field')))

def test_validate_named_field_value_just_attr(self):
class IFace(interface.Interface):
thing = interface.Attribute("A thing")

setter = INT.validate_named_field_value(self.Bag(), IFace, 'thing', 42)
setter()
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -14,7 +14,7 @@ basepython =
python2.7
commands =
coverage run -m zope.testrunner --test-path=src
coverage report --fail-under=99
coverage report --fail-under=100
deps =
{[testenv]deps}
coverage
Expand Down

0 comments on commit d1ea6e0

Please sign in to comment.