Skip to content

Commit

Permalink
Merge pull request #48 from zopefoundation/issue23
Browse files Browse the repository at this point in the history
Let subclasses of Collection and Object define attributes that can be…
  • Loading branch information
jamadden committed Aug 30, 2018
2 parents 19c9511 + ec47c09 commit b662565
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 301 deletions.
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@
to the collections ABCs of the same name; ``Tuple`` now extends
``Sequence`` and ``List`` now extends ``MutableSequence``.

- Add new field ``Collection``, implementing ``ICollection``. This is
the base class of ``Sequence``. Previously this was known as
``AbstractCollection`` and was not public. It can be subclassed to
add ``value_type``, ``_type`` and ``unique`` attributes at the class
level, enabling a simpler constructor call. See `issue 23
<https://github.com/zopefoundation/zope.schema/issues/23>`_.

- Make ``Object`` respect a ``schema`` attribute defined by a
subclass, enabling a simpler constructor call. See `issue 23
<https://github.com/zopefoundation/zope.schema/issues/23>`_.

4.5.0 (2017-07-10)
==================

Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Fields
======

.. autoclass:: zope.schema.Field
.. autoclass:: zope.schema.Collection
.. autoclass:: zope.schema._field.AbstractCollection
.. autoclass:: zope.schema.ASCII
:no-show-inheritance:
Expand Down
2 changes: 2 additions & 0 deletions src/zope/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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
Expand Down Expand Up @@ -75,6 +76,7 @@
'Bytes',
'BytesLine',
'Choice',
'Collection',
'Container',
'Date',
'Datetime',
Expand Down
11 changes: 10 additions & 1 deletion src/zope/schema/_bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
from zope.schema._schema import getFields


class _NotGiven(object):

def __repr__(self): # pragma: no cover
return "<Not Given>"


_NotGiven = _NotGiven()


class ValidatedProperty(object):

def __init__(self, name, check=None, allow_none=False):
Expand Down Expand Up @@ -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
Expand Down
84 changes: 59 additions & 25 deletions src/zope/schema/_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
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
Expand Down Expand Up @@ -99,6 +100,7 @@
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
Expand Down Expand Up @@ -133,6 +135,7 @@
classImplements(Int, IInt)



@implementer(ISourceText)
class SourceText(Text):
__doc__ = ISourceText.__doc__
Expand Down Expand Up @@ -538,29 +541,48 @@ 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:
clone.value_type = clone.value_type.bind(object)
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:
Expand All @@ -572,8 +594,15 @@ def _validate(self, value):
_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.
Expand Down Expand Up @@ -604,28 +633,26 @@ class List(MutableSequence):
_type = list


@implementer(ISet)
class Set(AbstractCollection):
"""A field representing a set."""
_type = set
class _AbstractSet(Collection):
unique = True

def __init__(self, **kw):
if 'unique' in kw: # set members are always unique
def __init__(self, *args, **kwargs):
super(_AbstractSet, self).__init__(*args, **kwargs)
if not self.unique: # set members are always unique
raise TypeError(
"__init__() got an unexpected keyword argument 'unique'")
super(Set, self).__init__(unique=True, **kw)


@implementer(ISet)
class Set(_AbstractSet):
"""A field representing a set."""
_type = set


@implementer(IFrozenSet)
class FrozenSet(AbstractCollection):
class FrozenSet(_AbstractSet):
_type = frozenset

def __init__(self, **kw):
if 'unique' in kw: # set members are always unique
raise TypeError(
"__init__() got an unexpected keyword argument 'unique'")
super(FrozenSet, self).__init__(unique=True, **kw)


VALIDATED_VALUES = threading.local()

Expand Down Expand Up @@ -677,18 +704,25 @@ 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`.
.. 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

Expand Down
Loading

0 comments on commit b662565

Please sign in to comment.