summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2018-09-02 08:22:22 -0500
committerJason Madden <jamadden@gmail.com>2018-09-02 08:26:46 -0500
commitaa637b2d1aae70d6425c1b80346a6ed17dc337ef (patch)
treea6656756a32a8768bb488728f6e381bd88196a56
parente163d536405e88623b115c7a9761e20ecf14159c (diff)
downloadzope-schema-issue17.tar.gz
Bind all fields of an Object's schema to the value being validated.issue17
Fixes #17 in a backwards compatible way.
-rw-r--r--CHANGES.rst6
-rw-r--r--src/zope/schema/_bootstrapfields.py4
-rw-r--r--src/zope/schema/_field.py56
-rw-r--r--src/zope/schema/tests/test__field.py74
4 files changed, 124 insertions, 16 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 5eada5d..16cd7b9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -107,6 +107,12 @@
- Make ``SimpleVocabulary`` and ``SimpleTerm`` have value-based
equality and hashing methods.
+- All fields of the schema of an ``Object`` field are bound to the
+ top-level value being validated before attempting validation of
+ their particular attribute. Previously only ``IChoice`` fields were
+ bound. See `issue 17
+ <https://github.com/zopefoundation/zope.schema/issues/17>`_.
+
4.5.0 (2017-07-10)
==================
diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py
index 5904068..70e2d93 100644
--- a/src/zope/schema/_bootstrapfields.py
+++ b/src/zope/schema/_bootstrapfields.py
@@ -187,10 +187,10 @@ class Field(Attribute):
def constraint(self, value):
return True
- def bind(self, object):
+ def bind(self, context):
clone = self.__class__.__new__(self.__class__)
clone.__dict__.update(self.__dict__)
- clone.context = object
+ clone.context = context
return clone
def validate(self, value):
diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py
index 5ec0d1b..db0b55d 100644
--- a/src/zope/schema/_field.py
+++ b/src/zope/schema/_field.py
@@ -379,15 +379,15 @@ class Choice(Field):
source = property(lambda self: self.vocabulary)
- def bind(self, object):
+ def bind(self, context):
"""See zope.schema._bootstrapinterfaces.IField."""
- clone = super(Choice, self).bind(object)
+ clone = super(Choice, self).bind(context)
# get registered vocabulary if needed:
if IContextSourceBinder.providedBy(self.vocabulary):
- clone.vocabulary = self.vocabulary(object)
+ clone.vocabulary = self.vocabulary(context)
elif clone.vocabulary is None and self.vocabularyName is not None:
vr = getVocabularyRegistry()
- clone.vocabulary = vr.get(object, self.vocabularyName)
+ clone.vocabulary = vr.get(context, self.vocabularyName)
if not ISource.providedBy(clone.vocabulary):
raise ValueError('Invalid clone vocabulary')
@@ -622,13 +622,13 @@ class Collection(MinMaxLen, Iterable):
if unique is not _NotGiven:
self.unique = unique
- def bind(self, object):
+ def bind(self, context):
"""See zope.schema._bootstrapinterfaces.IField."""
- clone = super(Collection, self).bind(object)
+ clone = super(Collection, self).bind(context)
# binding value_type is necessary for choices with named vocabularies,
# and possibly also for other fields.
if clone.value_type is not None:
- clone.value_type = clone.value_type.bind(object)
+ clone.value_type = clone.value_type.bind(context)
return clone
def _validate(self, value):
@@ -733,14 +733,11 @@ def _validate_fields(schema, value):
continue # pragma: no cover
try:
- if IChoice.providedBy(attribute):
- # Choice must be bound before validation otherwise
- # IContextSourceBinder is not iterable in validation
- bound = attribute.bind(value)
- bound.validate(getattr(value, name))
- elif IField.providedBy(attribute):
+ if IField.providedBy(attribute):
# validate attributes that are fields
attribute.validate(getattr(value, name))
+ # XXX: We're not even checking the existence of non-IField
+ # Attribute objects.
except ValidationError as error:
errors[name] = error
except AttributeError as error:
@@ -751,6 +748,37 @@ def _validate_fields(schema, value):
return errors
+class _BoundSchema(object):
+ """
+ This class proxies a schema to get its fields bound to a context.
+ """
+
+ __slots__ = ('schema', 'context')
+
+ def __new__(cls, schema, context):
+ # Only proxy if we really need to.
+ if schema is Interface or context is None:
+ return schema
+ return object.__new__(cls)
+
+ def __init__(self, schema, context):
+ self.schema = schema
+ self.context = context
+
+ def __getitem__(self, name):
+ # Indexing this item will bind fields,
+ # if possible
+ attr = self.schema[name]
+ try:
+ return attr.bind(self.context)
+ except AttributeError:
+ return attr
+
+ # but let all the rest slip to schema
+ def __getattr__(self, name):
+ return getattr(self.schema, name)
+
+
@implementer(IObject)
class Object(Field):
__doc__ = IObject.__doc__
@@ -788,7 +816,7 @@ class Object(Field):
raise SchemaNotProvided(self.schema, value).with_field_and_value(self, value)
# check the value against schema
- schema_error_dict = _validate_fields(self.schema, value)
+ schema_error_dict = _validate_fields(_BoundSchema(self.schema, value), value)
invariant_errors = []
if self.validate_invariants:
try:
diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py
index 0d12ad7..d208456 100644
--- a/src/zope/schema/tests/test__field.py
+++ b/src/zope/schema/tests/test__field.py
@@ -2005,6 +2005,80 @@ class ObjectTests(EqualityTestsMixin,
field.validate(ValueType())
+ def test_bound_field_of_collection_with_choice(self):
+ # https://github.com/zopefoundation/zope.schema/issues/17
+ from zope.interface import Interface, implementer
+ from zope.interface import Attribute
+
+ from zope.schema import Choice, Object, Set
+ from zope.schema.fieldproperty import FieldProperty
+ from zope.schema.interfaces import IContextSourceBinder
+ from zope.schema.interfaces import WrongContainedType
+ from zope.schema.interfaces import SchemaNotCorrectlyImplemented
+ from zope.schema.vocabulary import SimpleVocabulary
+
+
+ @implementer(IContextSourceBinder)
+ class EnumContext(object):
+ def __call__(self, context):
+ return SimpleVocabulary.fromValues(list(context))
+
+ class IMultipleChoice(Interface):
+ choices = Set(value_type=Choice(source=EnumContext()))
+ # Provide a regular attribute to prove that binding doesn't
+ # choke. NOTE: We don't actually verify the existence of this attribute.
+ non_field = Attribute("An attribute")
+
+ @implementer(IMultipleChoice)
+ class Choices(object):
+
+ def __init__(self, choices):
+ self.choices = choices
+
+ def __iter__(self):
+ # EnumContext calls this to make the vocabulary.
+ # Fields of the schema of the IObject are bound to the value being
+ # validated.
+ return iter(range(5))
+
+ class IFavorites(Interface):
+ fav = Object(title=u"Favorites number", schema=IMultipleChoice)
+
+
+ @implementer(IFavorites)
+ class Favorites(object):
+ fav = FieldProperty(IFavorites['fav'])
+
+ # must not raise
+ good_choices = Choices({1, 3})
+ IFavorites['fav'].validate(good_choices)
+
+ # Ranges outside the context fail
+ bad_choices = Choices({1, 8})
+ with self.assertRaises(WrongContainedType) as exc:
+ IFavorites['fav'].validate(bad_choices)
+
+ e = exc.exception
+ self.assertEqual(IFavorites['fav'], e.field)
+ self.assertEqual(bad_choices, e.value)
+
+ # Validation through field property
+ favorites = Favorites()
+ favorites.fav = good_choices
+
+ # And validation through a field that wants IFavorites
+ favorites_field = Object(IFavorites)
+ favorites_field.validate(favorites)
+
+ # Check the field property error
+ with self.assertRaises(SchemaNotCorrectlyImplemented) as exc:
+ favorites.fav = bad_choices
+
+ e = exc.exception
+ self.assertEqual(IFavorites['fav'], e.field)
+ self.assertEqual(bad_choices, e.value)
+ self.assertEqual(['choices'], list(e.schema_errors))
+
class MappingTests(EqualityTestsMixin,
unittest.TestCase):