diff options
| author | Jason Madden <jamadden@gmail.com> | 2018-09-02 08:22:22 -0500 |
|---|---|---|
| committer | Jason Madden <jamadden@gmail.com> | 2018-09-02 08:26:46 -0500 |
| commit | aa637b2d1aae70d6425c1b80346a6ed17dc337ef (patch) | |
| tree | a6656756a32a8768bb488728f6e381bd88196a56 | |
| parent | e163d536405e88623b115c7a9761e20ecf14159c (diff) | |
| download | zope-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.rst | 6 | ||||
| -rw-r--r-- | src/zope/schema/_bootstrapfields.py | 4 | ||||
| -rw-r--r-- | src/zope/schema/_field.py | 56 | ||||
| -rw-r--r-- | src/zope/schema/tests/test__field.py | 74 |
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): |
