summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2018-08-13 14:47:29 -0500
committerJason Madden <jamadden@gmail.com>2018-08-14 07:27:28 -0500
commite1e5686205f9223c3c98caa1e292ab3fae31740f (patch)
tree30c633e463633a3497284a4f1cdf1ab63fc2ac63 /src
parentc61de9872ad17fb279641784329266ad371d2366 (diff)
downloadzope-schema-e1e5686205f9223c3c98caa1e292ab3fae31740f.tar.gz
Add the ability for Object to check schema invariants.
Leave the ability to opt-out of that in case it causes unexpected breakage. Fixes #10.
Diffstat (limited to 'src')
-rw-r--r--src/zope/schema/_field.py23
-rw-r--r--src/zope/schema/interfaces.py13
-rw-r--r--src/zope/schema/tests/test__field.py106
3 files changed, 114 insertions, 28 deletions
diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py
index b4e6db7..815e58e 100644
--- a/src/zope/schema/_field.py
+++ b/src/zope/schema/_field.py
@@ -27,6 +27,7 @@ from zope.event import notify
from zope.interface import classImplements
from zope.interface import implementer
from zope.interface import Interface
+from zope.interface import Invalid
from zope.interface.interfaces import IInterface
from zope.interface.interfaces import IMethod
@@ -615,10 +616,21 @@ class Object(Field):
__doc__ = IObject.__doc__
def __init__(self, schema, **kw):
+ """
+ Object(schema, validate_invariants=True, **kwargs)
+
+ Create an `~.IObject` field. The keyword arguments are as for `~.Field`.
+
+ .. versionchanged:: 4.6.0
+ Add the keyword argument *validate_invariants*. When true (the default),
+ the schema's ``validateInvariants`` method will be invoked to check
+ the ``@invariant`` properties of the schema.
+ """
if not IInterface.providedBy(schema):
raise WrongType
self.schema = schema
+ self.validate_invariants = kw.pop('validate_invariants', True)
super(Object, self).__init__(**kw)
def _validate(self, value):
@@ -630,6 +642,17 @@ class Object(Field):
# check the value against schema
errors = _validate_fields(self.schema, value)
+
+ if self.validate_invariants:
+ try:
+ self.schema.validateInvariants(value, errors)
+ except Invalid:
+ # validateInvariants raises a wrapper error around
+ # all the errors it got if it got errors, in addition
+ # no appending them to the errors list. We don't want
+ # that, we raise our own error.
+ pass
+
if errors:
raise WrongContainedType(errors, self.__name__)
diff --git a/src/zope/schema/interfaces.py b/src/zope/schema/interfaces.py
index 1066fb8..6ed8169 100644
--- a/src/zope/schema/interfaces.py
+++ b/src/zope/schema/interfaces.py
@@ -535,13 +535,24 @@ class IFrozenSet(IAbstractSet):
class IObject(IField):
- """Field containing an Object value."""
+ """
+ Field containing an Object value.
+
+ .. versionchanged:: 4.6.0
+ Add the *validate_invariants* attribute.
+ """
schema = Attribute(
"schema",
_("The Interface that defines the Fields comprising the Object.")
)
+ validate_invariants = Attribute(
+ "validate_invariants",
+ _("A boolean that says whether ``schema.validateInvariants`` "
+ "is called from ``self.validate()``. The default is true.")
+ )
+
class IBeforeObjectAssignedEvent(Interface):
"""An object is going to be assigned to an attribute on another object.
diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py
index 9bcb116..7ce5ab4 100644
--- a/src/zope/schema/tests/test__field.py
+++ b/src/zope/schema/tests/test__field.py
@@ -17,6 +17,12 @@ import unittest
from zope.schema.tests.test__bootstrapfields import OrderableMissingValueMixin
+# pylint:disable=protected-access
+# pylint:disable=too-many-lines
+# pylint:disable=inherit-non-class
+# pylint:disable=no-member
+# pylint:disable=blacklisted-name
+
class BytesTests(unittest.TestCase):
def _getTargetClass(self):
@@ -73,7 +79,6 @@ class BytesTests(unittest.TestCase):
self.assertRaises(RequiredMissing, field.validate, None)
def test_fromUnicode_miss(self):
- from zope.schema._compat import text_type
byt = self._makeOne()
self.assertRaises(UnicodeEncodeError, byt.fromUnicode, u'\x81')
@@ -458,10 +463,9 @@ class DatetimeTests(OrderableMissingValueMixin,
self.assertRaises(WrongType, field.validate, date.today())
def test_validate_not_required(self):
- from datetime import datetime
field = self._makeOne(required=False)
field.validate(None) # doesn't raise
- field.validate(datetime.now()) # doesn't raise
+ field.validate(datetime.datetime.now()) # doesn't raise
def test_validate_required(self):
from zope.schema.interfaces import RequiredMissing
@@ -469,35 +473,32 @@ class DatetimeTests(OrderableMissingValueMixin,
self.assertRaises(RequiredMissing, field.validate, None)
def test_validate_w_min(self):
- from datetime import datetime
from zope.schema.interfaces import TooSmall
- d1 = datetime(2000, 10, 1)
- d2 = datetime(2000, 10, 2)
+ d1 = datetime.datetime(2000, 10, 1)
+ d2 = datetime.datetime(2000, 10, 2)
field = self._makeOne(min=d1)
field.validate(d1) # doesn't raise
field.validate(d2) # doesn't raise
- self.assertRaises(TooSmall, field.validate, datetime(2000, 9, 30))
+ self.assertRaises(TooSmall, field.validate, datetime.datetime(2000, 9, 30))
def test_validate_w_max(self):
- from datetime import datetime
from zope.schema.interfaces import TooBig
- d1 = datetime(2000, 10, 1)
- d2 = datetime(2000, 10, 2)
- d3 = datetime(2000, 10, 3)
+ d1 = datetime.datetime(2000, 10, 1)
+ d2 = datetime.datetime(2000, 10, 2)
+ d3 = datetime.datetime(2000, 10, 3)
field = self._makeOne(max=d2)
field.validate(d1) # doesn't raise
field.validate(d2) # doesn't raise
self.assertRaises(TooBig, field.validate, d3)
def test_validate_w_min_and_max(self):
- from datetime import datetime
from zope.schema.interfaces import TooBig
from zope.schema.interfaces import TooSmall
- d1 = datetime(2000, 10, 1)
- d2 = datetime(2000, 10, 2)
- d3 = datetime(2000, 10, 3)
- d4 = datetime(2000, 10, 4)
- d5 = datetime(2000, 10, 5)
+ d1 = datetime.datetime(2000, 10, 1)
+ d2 = datetime.datetime(2000, 10, 2)
+ d3 = datetime.datetime(2000, 10, 3)
+ d4 = datetime.datetime(2000, 10, 4)
+ d5 = datetime.datetime(2000, 10, 5)
field = self._makeOne(min=d2, max=d4)
field.validate(d2) # doesn't raise
field.validate(d3) # doesn't raise
@@ -530,10 +531,8 @@ class DateTests(OrderableMissingValueMixin,
verifyObject(IDate, self._makeOne())
def test_validate_wrong_types(self):
- from datetime import datetime
from zope.schema.interfaces import WrongType
-
field = self._makeOne()
self.assertRaises(WrongType, field.validate, u'')
self.assertRaises(WrongType, field.validate, u'')
@@ -545,7 +544,7 @@ class DateTests(OrderableMissingValueMixin,
self.assertRaises(WrongType, field.validate, set())
self.assertRaises(WrongType, field.validate, frozenset())
self.assertRaises(WrongType, field.validate, object())
- self.assertRaises(WrongType, field.validate, datetime.now())
+ self.assertRaises(WrongType, field.validate, datetime.datetime.now())
def test_validate_not_required(self):
from datetime import date
@@ -554,22 +553,20 @@ class DateTests(OrderableMissingValueMixin,
field.validate(date.today())
def test_validate_required(self):
- from datetime import datetime
from zope.schema.interfaces import RequiredMissing
field = self._makeOne()
- field.validate(datetime.now().date())
+ field.validate(datetime.datetime.now().date())
self.assertRaises(RequiredMissing, field.validate, None)
def test_validate_w_min(self):
from datetime import date
- from datetime import datetime
from zope.schema.interfaces import TooSmall
d1 = date(2000, 10, 1)
d2 = date(2000, 10, 2)
field = self._makeOne(min=d1)
field.validate(d1)
field.validate(d2)
- field.validate(datetime.now().date())
+ field.validate(datetime.datetime.now().date())
self.assertRaises(TooSmall, field.validate, date(2000, 9, 30))
def test_validate_w_max(self):
@@ -1970,6 +1967,64 @@ class ObjectTests(unittest.TestCase):
self.assertEqual(log[-1].name, 'field')
self.assertEqual(log[-1].context, inst)
+ def test_validates_invariants_by_default(self):
+ from zope.interface import invariant
+ from zope.interface import Interface
+ from zope.interface import implementer
+ from zope.interface import Invalid
+ from zope.schema import Text
+ from zope.schema import Bytes
+
+ class ISchema(Interface):
+
+ foo = Text()
+ bar = Bytes()
+
+ @invariant
+ def check_foo(o):
+ if o.foo == u'bar':
+ raise Invalid("Foo is not valid")
+
+ @invariant
+ def check_bar(o):
+ if o.bar == b'foo':
+ raise Invalid("Bar is not valid")
+
+ @implementer(ISchema)
+ class O(object):
+ foo = u''
+ bar = b''
+
+
+ field = self._makeOne(ISchema)
+ inst = O()
+
+ # Fine at first
+ field.validate(inst)
+
+ inst.foo = u'bar'
+ errors = self._getErrors(field.validate, inst)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].args[0], "Foo is not valid")
+
+ del inst.foo
+ inst.bar = b'foo'
+ errors = self._getErrors(field.validate, inst)
+ self.assertEqual(len(errors), 1)
+ self.assertEqual(errors[0].args[0], "Bar is not valid")
+
+ # Both invalid
+ inst.foo = u'bar'
+ errors = self._getErrors(field.validate, inst)
+ self.assertEqual(len(errors), 2)
+ errors.sort(key=lambda i: i.args)
+ self.assertEqual(errors[0].args[0], "Bar is not valid")
+ self.assertEqual(errors[1].args[0], "Foo is not valid")
+
+ # We can specifically ask for invariants to be turned off.
+ field = self._makeOne(ISchema, validate_invariants=False)
+ field.validate(inst)
+
class DictTests(unittest.TestCase):
@@ -2085,9 +2140,6 @@ def _makeSampleVocabulary():
from zope.interface import implementer
from zope.schema.interfaces import IVocabulary
- class SampleTerm(object):
- pass
-
@implementer(IVocabulary)
class SampleVocabulary(object):