summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2018-08-29 10:13:23 -0500
committerJason Madden <jamadden@gmail.com>2018-08-29 10:13:23 -0500
commit1cc0a04a9693fbb20cead0a958f951a5b837083d (patch)
treebaf7936d363d9dce2d63c2e1a3fa835aa22922fd /src
parent19c95112eea707be0e40166c59f0702a280a14f0 (diff)
downloadzope-schema-1cc0a04a9693fbb20cead0a958f951a5b837083d.tar.gz
Let subclasses of Collection and Object define attributes that can be omitted from constructor calls.issue23
Fixes #23
Diffstat (limited to 'src')
-rw-r--r--src/zope/schema/__init__.py2
-rw-r--r--src/zope/schema/_bootstrapfields.py11
-rw-r--r--src/zope/schema/_field.py62
-rw-r--r--src/zope/schema/tests/test__field.py78
4 files changed, 133 insertions, 20 deletions
diff --git a/src/zope/schema/__init__.py b/src/zope/schema/__init__.py
index 895459b..8fc6825 100644
--- a/src/zope/schema/__init__.py
+++ b/src/zope/schema/__init__.py
@@ -20,6 +20,7 @@ from zope.schema._field import Bool
from zope.schema._field import Bytes
from zope.schema._field import BytesLine
from zope.schema._field import Choice
+from zope.schema._field import Collection
from zope.schema._field import Container
from zope.schema._field import Date
from zope.schema._field import Datetime
@@ -75,6 +76,7 @@ __all__ = [
'Bytes',
'BytesLine',
'Choice',
+ 'Collection',
'Container',
'Date',
'Datetime',
diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py
index 6501310..f0174fe 100644
--- a/src/zope/schema/_bootstrapfields.py
+++ b/src/zope/schema/_bootstrapfields.py
@@ -39,6 +39,15 @@ from zope.schema._compat import integer_types
from zope.schema._schema import getFields
+class _NotGiven(object):
+
+ def __repr__(self):
+ return "<Not Given>"
+
+
+_NotGiven = _NotGiven()
+
+
class ValidatedProperty(object):
def __init__(self, name, check=None, allow_none=False):
@@ -97,7 +106,7 @@ class Field(Attribute):
# Field constructor. A marker is helpful since we don't want to
# overwrite missing_value if it is set differently on a Field
# subclass and isn't specified via the constructor.
- __missing_value_marker = object()
+ __missing_value_marker = _NotGiven
# Note that the "order" field has a dual existance:
# 1. The class variable Field.order is used as a source for the
diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py
index 9570ce8..86b2a26 100644
--- a/src/zope/schema/_field.py
+++ b/src/zope/schema/_field.py
@@ -45,6 +45,7 @@ from zope.schema.interfaces import IBool
from zope.schema.interfaces import IBytes
from zope.schema.interfaces import IBytesLine
from zope.schema.interfaces import IChoice
+from zope.schema.interfaces import ICollection
from zope.schema.interfaces import IContextSourceBinder
from zope.schema.interfaces import IDate
from zope.schema.interfaces import IDatetime
@@ -99,6 +100,7 @@ from zope.schema._bootstrapfields import Bool
from zope.schema._bootstrapfields import Int
from zope.schema._bootstrapfields import Password
from zope.schema._bootstrapfields import MinMaxLen
+from zope.schema._bootstrapfields import _NotGiven
from zope.schema.fieldproperty import FieldProperty
from zope.schema.vocabulary import getVocabularyRegistry
from zope.schema.vocabulary import VocabularyRegistryError
@@ -133,6 +135,7 @@ classImplements(Bool, IFromUnicode)
classImplements(Int, IInt)
+
@implementer(ISourceText)
class SourceText(Text):
__doc__ = ISourceText.__doc__
@@ -538,21 +541,40 @@ def _validate_uniqueness(self, value):
temp_values.append(item)
-class AbstractCollection(MinMaxLen, Iterable):
+@implementer(ICollection)
+class Collection(MinMaxLen, Iterable):
+ """
+ A generic collection implementing :class:`zope.schema.interfaces.ICollection`.
+
+ Subclasses can define the attribute ``value_type`` to be a field
+ such as an :class:`Object` that will be checked for each member of
+ the collection. This can then be omitted from the constructor call.
+
+ They can also define the attribute ``_type`` to be a concrete
+ class (or tuple of classes) that the collection itself will
+ be checked to be an instance of. This cannot be set in the constructor.
+
+ .. versionchanged:: 4.6.0
+ Add the ability for subclasses to specify ``value_type``
+ and ``unique``, and allow eliding them from the constructor.
+ """
value_type = None
unique = False
- def __init__(self, value_type=None, unique=False, **kw):
- super(AbstractCollection, self).__init__(**kw)
+ def __init__(self, value_type=_NotGiven, unique=_NotGiven, **kw):
+ super(Collection, self).__init__(**kw)
# whine if value_type is not a field
- if value_type is not None and not IField.providedBy(value_type):
+ if value_type is not _NotGiven:
+ self.value_type = value_type
+
+ if self.value_type is not None and not IField.providedBy(self.value_type):
raise ValueError("'value_type' must be field instance.")
- self.value_type = value_type
- self.unique = unique
+ if unique is not _NotGiven:
+ self.unique = unique
def bind(self, object):
"""See zope.schema._bootstrapinterfaces.IField."""
- clone = super(AbstractCollection, self).bind(object)
+ clone = super(Collection, self).bind(object)
# binding value_type is necessary for choices with named vocabularies,
# and possibly also for other fields.
if clone.value_type is not None:
@@ -560,7 +582,7 @@ class AbstractCollection(MinMaxLen, Iterable):
return clone
def _validate(self, value):
- super(AbstractCollection, self)._validate(value)
+ super(Collection, self)._validate(value)
errors = _validate_sequence(self.value_type, value)
if errors:
try:
@@ -572,8 +594,15 @@ class AbstractCollection(MinMaxLen, Iterable):
_validate_uniqueness(self, value)
+#: An alternate name for :class:`.Collection`.
+#:
+#: .. deprecated:: 4.6.0
+#: Use :class:`.Collection` instead.
+AbstractCollection = Collection
+
+
@implementer(ISequence)
-class Sequence(AbstractCollection):
+class Sequence(Collection):
"""
A field representing an ordered sequence.
@@ -605,7 +634,7 @@ class List(MutableSequence):
@implementer(ISet)
-class Set(AbstractCollection):
+class Set(Collection):
"""A field representing a set."""
_type = set
@@ -617,7 +646,7 @@ class Set(AbstractCollection):
@implementer(IFrozenSet)
-class FrozenSet(AbstractCollection):
+class FrozenSet(Collection):
_type = frozenset
def __init__(self, **kw):
@@ -677,10 +706,11 @@ def _validate_fields(schema, value):
@implementer(IObject)
class Object(Field):
__doc__ = IObject.__doc__
+ schema = None
- def __init__(self, schema, **kw):
+ def __init__(self, schema=_NotGiven, **kw):
"""
- Object(schema, *, validate_invariants=True, **kwargs)
+ Object(schema=<Not Given>, *, validate_invariants=True, **kwargs)
Create an `~.IObject` field. The keyword arguments are as for `~.Field`.
@@ -688,7 +718,13 @@ class Object(Field):
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
diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py
index dd9bd14..390cd71 100644
--- a/src/zope/schema/tests/test__field.py
+++ b/src/zope/schema/tests/test__field.py
@@ -1536,15 +1536,15 @@ class TupleTests(unittest.TestCase):
self.assertRaises(TooLong, field.validate, (1, 2, 3))
-class SequenceTests(unittest.TestCase):
+class AbstractCollectionTests(unittest.TestCase):
def _getTargetClass(self):
- from zope.schema._field import Sequence
- return Sequence
+ from zope.schema._field import AbstractCollection
+ return AbstractCollection
def _getTargetInterface(self):
- from zope.schema.interfaces import ISequence
- return ISequence
+ from zope.schema.interfaces import ICollection
+ return ICollection
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
@@ -1553,10 +1553,51 @@ class SequenceTests(unittest.TestCase):
from zope.interface.verify import verifyClass
verifyClass(self._getTargetInterface(), self._getTargetClass())
- def test_instance_conforms_to_IList(self):
+ def test_instance_conforms_to_iface(self):
from zope.interface.verify import verifyObject
verifyObject(self._getTargetInterface(), self._makeOne())
+
+ def test_schema_defined_by_subclass(self):
+ from zope import interface
+ from zope.schema import Object
+ from zope.schema.interfaces import WrongContainedType
+
+ class IValueType(interface.Interface):
+ "The value type schema"
+
+ the_value_type = Object(IValueType)
+
+ class Field(self._getTargetClass()):
+ value_type = the_value_type
+
+ field = Field()
+ self.assertIs(field.value_type, the_value_type)
+
+ # Empty collection is fine
+ field.validate([])
+
+ # Collection with a non-implemented object is bad
+ self.assertRaises(WrongContainedType, field.validate, [object()])
+
+ # Actual implementation works
+ @interface.implementer(IValueType)
+ class ValueType(object):
+ "The value type"
+
+
+ field.validate([ValueType()])
+
+class SequenceTests(AbstractCollectionTests):
+
+ def _getTargetClass(self):
+ from zope.schema._field import Sequence
+ return Sequence
+
+ def _getTargetInterface(self):
+ from zope.schema.interfaces import ISequence
+ return ISequence
+
def test_validate_wrong_types(self):
from zope.schema.interfaces import WrongType
@@ -2235,6 +2276,31 @@ class ObjectTests(unittest.TestCase):
field = self._makeOne(ISchema, validate_invariants=False)
field.validate(inst)
+ def test_schema_defined_by_subclass(self):
+ from zope import interface
+ from zope.schema import Object
+ from zope.schema.interfaces import SchemaNotProvided
+
+ class IValueType(interface.Interface):
+ "The value type schema"
+
+ class Field(self._getTargetClass()):
+ schema = IValueType
+
+ field = Field()
+ self.assertIs(field.schema, IValueType)
+
+ # Non implementation is bad
+ self.assertRaises(SchemaNotProvided, field.validate, object())
+
+ # Actual implementation works
+ @interface.implementer(IValueType)
+ class ValueType(object):
+ "The value type"
+
+
+ field.validate(ValueType())
+
class MappingTests(unittest.TestCase):