diff options
author | Jason Madden <jamadden@gmail.com> | 2018-08-29 10:13:23 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2018-08-29 10:13:23 -0500 |
commit | 1cc0a04a9693fbb20cead0a958f951a5b837083d (patch) | |
tree | baf7936d363d9dce2d63c2e1a3fa835aa22922fd /src | |
parent | 19c95112eea707be0e40166c59f0702a280a14f0 (diff) | |
download | zope-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__.py | 2 | ||||
-rw-r--r-- | src/zope/schema/_bootstrapfields.py | 11 | ||||
-rw-r--r-- | src/zope/schema/_field.py | 62 | ||||
-rw-r--r-- | src/zope/schema/tests/test__field.py | 78 |
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): |