summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2018-09-02 08:22:22 -0500
committerJason Madden <jamadden@gmail.com>2018-09-06 15:47:51 -0500
commitf1d76a77ce601fa75ffaf0ed345820950c4e318e (patch)
treebe1603e36b690dfa357df3b22b910405f4757974 /src
parenta409cbf2f88a19f9d4fc1bb0b8510634044bcc8c (diff)
downloadzope-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.py204
-rw-r--r--src/zope/schema/_bootstrapinterfaces.py56
-rw-r--r--src/zope/schema/_field.py155
-rw-r--r--src/zope/schema/_schema.py107
-rw-r--r--src/zope/schema/interfaces.py198
-rw-r--r--src/zope/schema/tests/test__field.py74
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):