diff options
author | Jason Madden <jamadden@gmail.com> | 2018-09-02 08:22:22 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2018-09-06 15:47:51 -0500 |
commit | f1d76a77ce601fa75ffaf0ed345820950c4e318e (patch) | |
tree | be1603e36b690dfa357df3b22b910405f4757974 /src | |
parent | a409cbf2f88a19f9d4fc1bb0b8510634044bcc8c (diff) | |
download | zope-schema-f1d76a77ce601fa75ffaf0ed345820950c4e318e.tar.gz |
Make Object a bootstrapfield and share the logic between Object validation and the public functions get[Schema]ValidationErrors.
Fixes #57
This makes Object bind all fields (not just choices) before
validation. This fixes #17 in a backwards compatible way.
Binding all attributes, not just Choices, reduced the dependencies of
Object and facilitated making it a bootstrap field. Making it a
bootstrap field in turn let us use it in more places in interfaces.py,
which fixes #13.
Switch from just repeating attribute names in interfaces.py to a real
__all__ attribute that linters can warn about.
Change ..autoclass:: in api.rst to ..autointerface:: so we get the
actual member documentation and not lots of warnings about missing
__mro__. (This is unrelated, I was just tired of the warnings.)
Diffstat (limited to 'src')
-rw-r--r-- | src/zope/schema/_bootstrapfields.py | 204 | ||||
-rw-r--r-- | src/zope/schema/_bootstrapinterfaces.py | 56 | ||||
-rw-r--r-- | src/zope/schema/_field.py | 155 | ||||
-rw-r--r-- | src/zope/schema/_schema.py | 107 | ||||
-rw-r--r-- | src/zope/schema/interfaces.py | 198 | ||||
-rw-r--r-- | src/zope/schema/tests/test__field.py | 74 |
6 files changed, 519 insertions, 275 deletions
diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py index 5904068..29fec18 100644 --- a/src/zope/schema/_bootstrapfields.py +++ b/src/zope/schema/_bootstrapfields.py @@ -18,31 +18,41 @@ __docformat__ = 'restructuredtext' import decimal import fractions import numbers +import threading from math import isinf from zope.interface import Attribute +from zope.interface import Invalid +from zope.interface import Interface from zope.interface import providedBy from zope.interface import implementer +from zope.interface.interfaces import IInterface +from zope.interface.interfaces import IMethod + +from zope.event import notify -from zope.schema._bootstrapinterfaces import ValidationError from zope.schema._bootstrapinterfaces import ConstraintNotSatisfied +from zope.schema._bootstrapinterfaces import IBeforeObjectAssignedEvent from zope.schema._bootstrapinterfaces import IContextAwareDefaultFactory from zope.schema._bootstrapinterfaces import IFromUnicode +from zope.schema._bootstrapinterfaces import IValidatable from zope.schema._bootstrapinterfaces import NotAContainer from zope.schema._bootstrapinterfaces import NotAnIterator from zope.schema._bootstrapinterfaces import RequiredMissing +from zope.schema._bootstrapinterfaces import SchemaNotCorrectlyImplemented +from zope.schema._bootstrapinterfaces import SchemaNotFullyImplemented +from zope.schema._bootstrapinterfaces import SchemaNotProvided from zope.schema._bootstrapinterfaces import StopValidation from zope.schema._bootstrapinterfaces import TooBig from zope.schema._bootstrapinterfaces import TooLong from zope.schema._bootstrapinterfaces import TooShort from zope.schema._bootstrapinterfaces import TooSmall +from zope.schema._bootstrapinterfaces import ValidationError from zope.schema._bootstrapinterfaces import WrongType from zope.schema._compat import text_type from zope.schema._compat import integer_types -from zope.schema._schema import getFields - class _NotGiven(object): @@ -98,6 +108,17 @@ class DefaultProperty(ValidatedProperty): return value +def getFields(schema): + """Return a dictionary containing all the Fields in a schema. + """ + fields = {} + for name in schema: + attr = schema[name] + if IValidatable.providedBy(attr): + fields[name] = attr + return fields + + class Field(Attribute): # Type restrictions, if any @@ -187,10 +208,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): @@ -655,3 +676,176 @@ class Int(Integral): """ _type = integer_types _unicode_converters = (int,) + + +VALIDATED_VALUES = threading.local() + +def get_schema_validation_errors(schema, value): + """ + Validate that *value* conforms to the schema interface *schema*. + + All :class:`zope.schema.interfaces.IField` members of the *schema* + are validated after being bound to *value*. (Note that we do not check for + arbitrary :class:`zope.interface.Attribute` members being present.) + + :return: A `dict` mapping field names to `ValidationError` subclasses. + A non-empty return value means that validation failed. + """ + errors = {} + # Interface can be used as schema property for Object fields that plan to + # hold values of any type. + # Because Interface does not include any Attribute, it is obviously not + # worth looping on its methods and filter them all out. + if schema is Interface: + return errors + # if `value` is part of a cyclic graph, we need to break the cycle to avoid + # infinite recursion. Collect validated objects in a thread local dict by + # it's python represenation. A previous version was setting a volatile + # attribute which didn't work with security proxy + if id(value) in VALIDATED_VALUES.__dict__: + return errors + VALIDATED_VALUES.__dict__[id(value)] = True + # (If we have gotten here, we know that `value` provides an interface + # other than zope.interface.Interface; + # iow, we can rely on the fact that it is an instance + # that supports attribute assignment.) + + try: + for name in schema.names(all=True): + attribute = schema[name] + if IMethod.providedBy(attribute): + continue # pragma: no cover + + try: + if IValidatable.providedBy(attribute): + # validate attributes that are fields + field_value = getattr(value, name) + attribute = attribute.bind(value) + attribute.validate(field_value) + except ValidationError as error: + errors[name] = error + except AttributeError as error: + # property for the given name is not implemented + errors[name] = SchemaNotFullyImplemented(error).with_field_and_value(attribute, None) + finally: + del VALIDATED_VALUES.__dict__[id(value)] + return errors + + +def get_validation_errors(schema, value, validate_invariants=True): + """ + Validate that *value* conforms to the schema interface *schema*. + + This includes checking for any schema validation errors (using + `get_schema_validation_errors`). If that succeeds, and + *validate_invariants* is true, then we proceed to check for any + declared invariants. + + Note that this does not include a check to see if the *value* + actually provides the given *schema*. + + :return: If there were any validation errors, either schema or + invariant, return a two tuple (schema_error_dict, + invariant_error_list). If there were no errors, returns a + two-tuple where both members are empty. + """ + schema_error_dict = get_schema_validation_errors(schema, value) + invariant_errors = [] + # Only validate invariants if there were no previous errors. Previous + # errors could be missing attributes which would most likely make an + # invariant raise an AttributeError. + + if validate_invariants and not schema_error_dict: + try: + schema.validateInvariants(value, invariant_errors) + except Invalid: + # validateInvariants raises a wrapper error around + # all the errors it got if it got errors, in addition + # to appending them to the errors list. We don't want + # that, we raise our own error. + pass + + return (schema_error_dict, invariant_errors) + + +class Object(Field): + """ + Implementation of :class:`zope.schema.interfaces.IObject`. + """ + schema = None + + def __init__(self, schema=_NotGiven, **kw): + """ + Object(schema=<Not Given>, *, 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. + .. versionchanged:: 4.6.0 + The *schema* argument can be ommitted in a subclass + that specifies a ``schema`` attribute. + """ + if schema is _NotGiven: + schema = self.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): + super(Object, self)._validate(value) + + # schema has to be provided by value + if not self.schema.providedBy(value): + raise SchemaNotProvided(self.schema, value).with_field_and_value(self, value) + + # check the value against schema + schema_error_dict, invariant_errors = get_validation_errors( + self.schema, + value, + self.validate_invariants + ) + + if schema_error_dict or invariant_errors: + errors = list(schema_error_dict.values()) + invariant_errors + exception = SchemaNotCorrectlyImplemented( + errors, + self.__name__ + ).with_field_and_value(self, value) + exception.schema_errors = schema_error_dict + exception.invariant_errors = invariant_errors + try: + raise exception + finally: + # Break cycles + del exception + del invariant_errors + del schema_error_dict + del errors + + def set(self, object, value): + # Announce that we're going to assign the value to the object. + # Motivation: Widgets typically like to take care of policy-specific + # actions, like establishing location. + event = BeforeObjectAssignedEvent(value, self.__name__, object) + notify(event) + # The event subscribers are allowed to replace the object, thus we need + # to replace our previous value. + value = event.object + super(Object, self).set(object, value) + + +@implementer(IBeforeObjectAssignedEvent) +class BeforeObjectAssignedEvent(object): + """An object is going to be assigned to an attribute on another object.""" + + def __init__(self, object, name, context): + self.object = object + self.name = name + self.context = context diff --git a/src/zope/schema/_bootstrapinterfaces.py b/src/zope/schema/_bootstrapinterfaces.py index 40d85e2..eeb4f0e 100644 --- a/src/zope/schema/_bootstrapinterfaces.py +++ b/src/zope/schema/_bootstrapinterfaces.py @@ -16,6 +16,7 @@ from functools import total_ordering import zope.interface +from zope.interface import Attribute from zope.schema._messageid import _ @@ -108,6 +109,31 @@ class NotAnIterator(ValidationError): __doc__ = _("""Not an iterator""") + +class WrongContainedType(ValidationError): + __doc__ = _("""Wrong contained type""") + + +class SchemaNotCorrectlyImplemented(WrongContainedType): + __doc__ = _("""An object failed schema or invariant validation.""") + + #: A dictionary mapping failed attribute names of the + #: *value* to the underlying exception + schema_errors = None + + #: A list of exceptions from validating the invariants + #: of the schema. + invariant_errors = () + + +class SchemaNotFullyImplemented(ValidationError): + __doc__ = _("""Schema not fully implemented""") + + +class SchemaNotProvided(ValidationError): + __doc__ = _("""Schema not provided""") + + class IFromUnicode(zope.interface.Interface): """Parse a unicode string to a value @@ -132,6 +158,36 @@ class IContextAwareDefaultFactory(zope.interface.Interface): """Returns a default value for the field.""" +class IBeforeObjectAssignedEvent(zope.interface.Interface): + """An object is going to be assigned to an attribute on another object. + + Subscribers to this event can change the object on this event to change + what object is going to be assigned. This is useful, e.g. for wrapping + or replacing objects before they get assigned to conform to application + policy. + """ + + object = Attribute("The object that is going to be assigned.") + + name = Attribute("The name of the attribute under which the object " + "will be assigned.") + + context = Attribute("The context object where the object will be " + "assigned to.") + + +class IValidatable(zope.interface.Interface): + # Internal interface, the base for IField, but used to prevent + # import recursion. This should *not* be implemented by anything + # other than IField. + def validate(value): + """Validate that the given value is a valid field value. + + Returns nothing but raises an error if the value is invalid. + It checks everything specific to a Field and also checks + with the additional constraint. + """ + class NO_VALUE(object): def __repr__(self): # pragma: no cover return '<NO_VALUE>' diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index c9dcd4b..f659877 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -27,20 +27,16 @@ from datetime import timedelta from datetime import time import decimal import re -import threading -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 + from zope.schema.interfaces import IASCII from zope.schema.interfaces import IASCIILine from zope.schema.interfaces import IBaseVocabulary -from zope.schema.interfaces import IBeforeObjectAssignedEvent from zope.schema.interfaces import IBool from zope.schema.interfaces import IBytes from zope.schema.interfaces import IBytesLine @@ -89,9 +85,6 @@ from zope.schema.interfaces import InvalidValue from zope.schema.interfaces import WrongType from zope.schema.interfaces import WrongContainedType from zope.schema.interfaces import NotUnique -from zope.schema.interfaces import SchemaNotProvided -from zope.schema.interfaces import SchemaNotCorrectlyImplemented -from zope.schema.interfaces import SchemaNotFullyImplemented from zope.schema.interfaces import InvalidURI from zope.schema.interfaces import InvalidId from zope.schema.interfaces import InvalidDottedName @@ -113,6 +106,7 @@ from zope.schema._bootstrapfields import Rational from zope.schema._bootstrapfields import Real from zope.schema._bootstrapfields import MinMaxLen from zope.schema._bootstrapfields import _NotGiven +from zope.schema._bootstrapfields import Object from zope.schema.fieldproperty import FieldProperty from zope.schema.vocabulary import getVocabularyRegistry from zope.schema.vocabulary import SimpleVocabulary @@ -150,6 +144,8 @@ classImplements(Rational, IRational) classImplements(Integral, IIntegral) classImplements(Int, IInt) +classImplements(Object, IObject) + @implementer(ISourceText) @@ -646,13 +642,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): @@ -728,141 +724,6 @@ class FrozenSet(_AbstractSet): _type = frozenset -VALIDATED_VALUES = threading.local() - - -def _validate_fields(schema, value): - errors = {} - # Interface can be used as schema property for Object fields that plan to - # hold values of any type. - # Because Interface does not include any Attribute, it is obviously not - # worth looping on its methods and filter them all out. - if schema is Interface: - return errors - # if `value` is part of a cyclic graph, we need to break the cycle to avoid - # infinite recursion. Collect validated objects in a thread local dict by - # it's python represenation. A previous version was setting a volatile - # attribute which didn't work with security proxy - if id(value) in VALIDATED_VALUES.__dict__: - return errors - VALIDATED_VALUES.__dict__[id(value)] = True - # (If we have gotten here, we know that `value` provides an interface - # other than zope.interface.Interface; - # iow, we can rely on the fact that it is an instance - # that supports attribute assignment.) - try: - for name in schema.names(all=True): - attribute = schema[name] - if IMethod.providedBy(attribute): - 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): - # validate attributes that are fields - attribute.validate(getattr(value, name)) - except ValidationError as error: - errors[name] = error - except AttributeError as error: - # property for the given name is not implemented - errors[name] = SchemaNotFullyImplemented(error).with_field_and_value(attribute, None) - finally: - del VALIDATED_VALUES.__dict__[id(value)] - return errors - - -@implementer(IObject) -class Object(Field): - __doc__ = IObject.__doc__ - schema = None - - def __init__(self, schema=_NotGiven, **kw): - """ - Object(schema=<Not Given>, *, 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. - .. versionchanged:: 4.6.0 - The *schema* argument can be ommitted in a subclass - that specifies a ``schema`` attribute. - """ - if schema is _NotGiven: - schema = self.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): - super(Object, self)._validate(value) - - # schema has to be provided by value - if not self.schema.providedBy(value): - raise SchemaNotProvided(self.schema, value).with_field_and_value(self, value) - - # check the value against schema - schema_error_dict = _validate_fields(self.schema, value) - invariant_errors = [] - if self.validate_invariants: - try: - self.schema.validateInvariants(value, invariant_errors) - except Invalid: - # validateInvariants raises a wrapper error around - # all the errors it got if it got errors, in addition - # to appending them to the errors list. We don't want - # that, we raise our own error. - pass - - if schema_error_dict or invariant_errors: - errors = list(schema_error_dict.values()) + invariant_errors - exception = SchemaNotCorrectlyImplemented( - errors, - self.__name__ - ).with_field_and_value(self, value) - exception.schema_errors = schema_error_dict - exception.invariant_errors = invariant_errors - try: - raise exception - finally: - # Break cycles - del exception - del invariant_errors - del schema_error_dict - del errors - - def set(self, object, value): - # Announce that we're going to assign the value to the object. - # Motivation: Widgets typically like to take care of policy-specific - # actions, like establishing location. - event = BeforeObjectAssignedEvent(value, self.__name__, object) - notify(event) - # The event subscribers are allowed to replace the object, thus we need - # to replace our previous value. - value = event.object - super(Object, self).set(object, value) - - -@implementer(IBeforeObjectAssignedEvent) -class BeforeObjectAssignedEvent(object): - """An object is going to be assigned to an attribute on another object.""" - - def __init__(self, object, name, context): - self.object = object - self.name = name - self.context = context - - @implementer(IMapping) class Mapping(MinMaxLen, Iterable): """ diff --git a/src/zope/schema/_schema.py b/src/zope/schema/_schema.py index 50b3a49..b64b7b6 100644 --- a/src/zope/schema/_schema.py +++ b/src/zope/schema/_schema.py @@ -14,26 +14,24 @@ """Schema convenience functions """ -import zope.interface.verify +from zope.schema._bootstrapfields import get_validation_errors +from zope.schema._bootstrapfields import get_schema_validation_errors +from zope.schema._bootstrapfields import getFields + +__all__ = [ + 'getFieldNames', + 'getFields', + 'getFieldsInOrder', + 'getFieldNamesInOrder', + 'getValidationErrors', + 'getSchemaValidationErrors', +] def getFieldNames(schema): """Return a list of all the Field names in a schema. """ - from zope.schema.interfaces import IField - return [name for name in schema if IField.providedBy(schema[name])] - - -def getFields(schema): - """Return a dictionary containing all the Fields in a schema. - """ - from zope.schema.interfaces import IField - fields = {} - for name in schema: - attr = schema[name] - if IField.providedBy(attr): - fields[name] = attr - return fields + return list(getFields(schema).keys()) def getFieldsInOrder(schema, _field_key=lambda x: x[1].order): @@ -48,44 +46,43 @@ def getFieldNamesInOrder(schema): return [name for name, field in getFieldsInOrder(schema)] -def getValidationErrors(schema, object): - """Return a list of all validation errors. +def getValidationErrors(schema, value): + """ + Validate that *value* conforms to the schema interface *schema*. + + This includes checking for any schema validation errors (using + `getSchemaValidationErrors`). If that succeeds, then we proceed to + check for any declared invariants. + + Note that this does not include a check to see if the *value* + actually provides the given *schema*. + + :return: A sequence of (name, `zope.interface.Invalid`) tuples, + where *name* is None if the error was from an invariant. + If the sequence is empty, there were no errors. + """ + schema_error_dict, invariant_errors = get_validation_errors( + schema, + value, + ) + + if not schema_error_dict and not invariant_errors: + # Valid! Yay! + return [] + + return list(schema_error_dict.items()) + [(None, e) for e in invariant_errors] + + +def getSchemaValidationErrors(schema, value): + """ + Validate that *value* conforms to the schema interface *schema*. + + All :class:`zope.schema.interfaces.IField` members of the *schema* + are validated after being bound to *value*. (Note that we do not check for + arbitrary :class:`zope.interface.Attribute` members being present.) + + :return: A sequence of (name, `ValidationError`) tuples. A non-empty + sequence indicates validation failed. """ - errors = getSchemaValidationErrors(schema, object) - if errors: - return errors - - # Only validate invariants if there were no previous errors. Previous - # errors could be missing attributes which would most likely make an - # invariant raise an AttributeError. - invariant_errors = [] - try: - schema.validateInvariants(object, invariant_errors) - except zope.interface.exceptions.Invalid: - # Just collect errors - pass - errors = [(None, e) for e in invariant_errors] - return errors - - -def getSchemaValidationErrors(schema, object): - errors = [] - for name in schema.names(all=True): - if zope.interface.interfaces.IMethod.providedBy(schema[name]): - continue - attribute = schema[name] - if not zope.schema.interfaces.IField.providedBy(attribute): - continue - try: - value = getattr(object, name) - except AttributeError as error: - # property for the given name is not implemented - error = zope.schema.interfaces.SchemaNotFullyImplemented(error) - error = error.with_field_and_value(attribute, None) - errors.append((name, error)) - else: - try: - attribute.bind(object).validate(value) - except zope.schema.ValidationError as e: - errors.append((name, e)) - return errors + items = get_schema_validation_errors(schema, value).items() + return items if isinstance(items, list) else list(items) diff --git a/src/zope/schema/interfaces.py b/src/zope/schema/interfaces.py index 88988c4..88e556b 100644 --- a/src/zope/schema/interfaces.py +++ b/src/zope/schema/interfaces.py @@ -15,12 +15,12 @@ """ __docformat__ = "reStructuredText" -from zope.interface import Interface, Attribute +from zope.interface import Interface +from zope.interface import Attribute +from zope.interface.interfaces import IInterface from zope.interface.common.mapping import IEnumerableMapping -# Import from _bootstrapinterfaces only because other packages will expect -# to find these interfaces here. from zope.schema._bootstrapfields import Field from zope.schema._bootstrapfields import Text from zope.schema._bootstrapfields import TextLine @@ -31,6 +31,10 @@ from zope.schema._bootstrapfields import Rational from zope.schema._bootstrapfields import Real from zope.schema._bootstrapfields import Integral from zope.schema._bootstrapfields import Int +from zope.schema._bootstrapfields import Object + +# Import from _bootstrapinterfaces only because other packages will expect +# to find these interfaces here. from zope.schema._bootstrapinterfaces import StopValidation from zope.schema._bootstrapinterfaces import ValidationError from zope.schema._bootstrapinterfaces import IFromUnicode @@ -44,47 +48,118 @@ from zope.schema._bootstrapinterfaces import TooBig from zope.schema._bootstrapinterfaces import TooLong from zope.schema._bootstrapinterfaces import TooShort from zope.schema._bootstrapinterfaces import InvalidValue +from zope.schema._bootstrapinterfaces import WrongContainedType +from zope.schema._bootstrapinterfaces import SchemaNotCorrectlyImplemented +from zope.schema._bootstrapinterfaces import SchemaNotFullyImplemented +from zope.schema._bootstrapinterfaces import SchemaNotProvided +from zope.schema._bootstrapinterfaces import IBeforeObjectAssignedEvent from zope.schema._bootstrapinterfaces import IContextAwareDefaultFactory +from zope.schema._bootstrapinterfaces import IValidatable from zope.schema._compat import PY3 from zope.schema._messageid import _ - -# pep 8 friendlyness -StopValidation, ValidationError, IFromUnicode, RequiredMissing, WrongType -ConstraintNotSatisfied, NotAContainer, NotAnIterator -TooSmall, TooBig, TooLong, TooShort, InvalidValue, IContextAwareDefaultFactory - - -class WrongContainedType(ValidationError): - __doc__ = _("""Wrong contained type""") - - -class SchemaNotCorrectlyImplemented(WrongContainedType): - __doc__ = _("""An object failed schema or invariant validation.""") - - #: A dictionary mapping failed attribute names of the - #: *value* to the underlying exception - schema_errors = None - - #: A list of exceptions from validating the invariants - #: of the schema. - invariant_errors = () +__all__ = [ + # Exceptions + 'ConstraintNotSatisfied', + 'InvalidDottedName', + 'InvalidId', + 'InvalidURI', + 'InvalidValue', + 'NotAContainer', + 'NotAnIterator', + 'NotUnique', + 'RequiredMissing', + 'SchemaNotCorrectlyImplemented', + 'SchemaNotFullyImplemented', + 'SchemaNotProvided', + 'StopValidation', + 'TooBig', + 'TooLong', + 'TooShort', + 'TooSmall', + 'Unbound', + 'ValidationError', + 'WrongContainedType', + 'WrongType', + + # Interfaces + 'IASCII', + 'IASCIILine', + 'IAbstractBag', + 'IAbstractSet', + 'IBaseVocabulary', + 'IBeforeObjectAssignedEvent', + 'IBool', + 'IBytes', + 'IBytesLine', + 'IChoice', + 'ICollection', + 'IComplex', + 'IContainer', + 'IContextAwareDefaultFactory', + 'IContextSourceBinder', + 'IDate', + 'IDatetime', + 'IDecimal', + 'IDict', + 'IDottedName', + 'IField', + 'IFieldEvent', + 'IFieldUpdatedEvent', + 'IFloat', + 'IFromUnicode', + 'IFrozenSet', + 'IId', + 'IInt', + 'IIntegral', + 'IInterfaceField', + 'IIterable', + 'IIterableSource', + 'IIterableVocabulary', + 'ILen', + 'IList', + 'IMapping', + 'IMinMax', + 'IMinMaxLen', + 'IMutableMapping', + 'IMutableSequence', + 'INativeString', + 'INativeStringLine', + 'INumber', + 'IObject', + 'IOrderable', + 'IPassword', + 'IRational', + 'IReal', + 'ISequence', + 'ISet', + 'ISource', + 'ISourceQueriables', + 'ISourceText', + 'ITerm', + 'IText', + 'ITextLine', + 'ITime', + 'ITimedelta', + 'ITitledTokenizedTerm', + 'ITokenizedTerm', + 'ITreeVocabulary', + 'ITuple', + 'IURI', + 'IUnorderedCollection', + 'IVocabulary', + 'IVocabularyFactory', + 'IVocabularyRegistry', + 'IVocabularyTokenized', +] class NotUnique(ValidationError): __doc__ = _("""One or more entries of sequence are not unique.""") -class SchemaNotFullyImplemented(ValidationError): - __doc__ = _("""Schema not fully implemented""") - - -class SchemaNotProvided(ValidationError): - __doc__ = _("""Schema not provided""") - - class InvalidURI(ValidationError): __doc__ = _("""The specified URI is not valid.""") @@ -101,7 +176,7 @@ class Unbound(Exception): __doc__ = _("""The field is not bound.""") -class IField(Interface): +class IField(IValidatable): """Basic Schema Field Interface. Fields are used for Interface specifications. They at least provide @@ -639,14 +714,14 @@ class IChoice(IField): # Abstract - class ICollection(IMinMaxLen, IIterable, IContainer): """Abstract interface containing a collection value. The Value must be iterable and may have a min_length/max_length. """ - value_type = Field( + value_type = Object( + IField, title=_("Value Type"), description=_("Field value items must conform to the given type, " "expressed via a Field.")) @@ -677,14 +752,14 @@ class IUnorderedCollection(ICollection): class IAbstractSet(IUnorderedCollection): """An unordered collection of unique values.""" - unique = Attribute("This ICollection interface attribute must be True") + unique = Bool(description="This ICollection interface attribute must be True") class IAbstractBag(IUnorderedCollection): """An unordered collection of values, with no limitations on whether members are unique""" - unique = Attribute("This ICollection interface attribute must be False") + unique = Bool(description="This ICollection interface attribute must be False") # Concrete @@ -720,34 +795,19 @@ class IObject(IField): Add the *validate_invariants* attribute. """ - schema = Attribute( - "schema", - _("The Interface that defines the Fields comprising the Object.") + schema = Object( + IInterface, + description=_("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.") + validate_invariants = Bool( + title="validate_invariants", + description=_("A boolean that says whether ``schema.validateInvariants`` " + "is called from ``self.validate()``. The default is true."), + default=True, ) -class IBeforeObjectAssignedEvent(Interface): - """An object is going to be assigned to an attribute on another object. - - Subscribers to this event can change the object on this event to change - what object is going to be assigned. This is useful, e.g. for wrapping - or replacing objects before they get assigned to conform to application - policy. - """ - - object = Attribute("The object that is going to be assigned.") - - name = Attribute("The name of the attribute under which the object " - "will be assigned.") - - context = Attribute("The context object where the object will be " - "assigned to.") class IMapping(IMinMaxLen, IIterable, IContainer): """ @@ -757,15 +817,15 @@ class IMapping(IMinMaxLen, IIterable, IContainer): of restrictions for keys and values contained in the dict. """ - key_type = Attribute( - "key_type", - _("Field keys must conform to the given type, expressed via a Field.") + key_type = Object( + IField, + description=_("Field keys must conform to the given type, expressed via a Field.") ) - value_type = Attribute( - "value_type", - _("Field values must conform to the given type, expressed " - "via a Field.") + value_type = Object( + IField, + description=_("Field values must conform to the given type, expressed " + "via a Field.") ) @@ -973,7 +1033,9 @@ class IVocabularyFactory(Interface): class IFieldEvent(Interface): - field = Attribute("The field that has been changed") + field = Object( + IField, + description="The field that has been changed") object = Attribute("The object containing the field") diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index e959688..7224d4a 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -2045,6 +2045,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): |