diff options
author | Jason Madden <jamadden@gmail.com> | 2018-08-13 14:47:29 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2018-08-14 07:27:28 -0500 |
commit | e1e5686205f9223c3c98caa1e292ab3fae31740f (patch) | |
tree | 30c633e463633a3497284a4f1cdf1ab63fc2ac63 /src | |
parent | c61de9872ad17fb279641784329266ad371d2366 (diff) | |
download | zope-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.py | 23 | ||||
-rw-r--r-- | src/zope/schema/interfaces.py | 13 | ||||
-rw-r--r-- | src/zope/schema/tests/test__field.py | 106 |
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): |