[Zope3-dev] Forget what I just said: a revised collection proposal

Gary Poster gary at modernsongs.com
Wed May 5 17:54:47 EDT 2004


After the last email I sent about collection fields, I've been through a 
couple of idea revisions, a couple of code revisions, and further 
conversations with Fred, Zac, Casey, and Stephan.  I have some different 
changes to propose on the basis of this churn.  I also have a Zope 3 
sandbox in which these changes are implemented, tested, and documented.

I have attached the new text documentation to this email for any who 
wish to comment on the changes.  The fields.txt is documentation for the 
zope.schema package, and the README.txt is documentation for the 
zope.app.form.browser package.

The changes continue along the simplification that Stephan began, while 
keeping the sophistication--or the capability for sophistication--that 
we need for our apps.  Stephan, this is even simpler than what I 
proposed last to you, and I think proceeds in the direction you prefer.

The change perhaps most surprising in light of my last email to the list 
is that I have removed the Sequence field, and also do not include the 
proposed Collection field.  The abstract ICollection, ISequence and 
IUnorderedCollection interfaces are present to describe the hierarchy of 
behavior.  The only collection fields available are concrete, however: 
List, Tuple, and Set.  (Bag is currently a WHUI--"We Haven't Used 
It"--but may be reintroduced as soon as someone wants a schema-based 
shopping cart :-)

The generic "Sequence" and "Collection" fields are omitted for at least 
a couple of reasons:

    * All of the other fields specify the API of the attribute: a
      TextLine specifies a Unicode API, an Int specifies an int API, but
      a Sequence or a Collection give (almost) no information about the
      API needed to access the collection's contents (almost because
      they do specify that  they are iterable).
    * Sequence had to make a choice as to what it used--a list. 
      Therefore, effectively "Sequence" meant "List", plus a side of
      hand waving.

These are unnecessary and problematic punts.  I don't think they belong 
in the core.

In my sandbox, I've converted all uses of Sequence--they were in 
IDublinCore only, if I remember correctly--to List.  The webdav widgets 
still are registered for ISequence because that still is pertinent, I 
believe (although I'll probably be asking for confirmation on this.  The 
special IChoiceSequence marker interface Stephan introduced is removed 
so that our widget usage patterns do not unnecessarily affect the schema 
package.  The widget registrations are changed to match the pattern 
described in the README.txt document.

Please express any concerns.  I'd like to check this in tomorrow.  If I 
don't hear any big concerns, I probably will. :-)

Gary
-------------- next part --------------
===============
Browser Widgets
===============

This directory contains widgets: views on bound schema fields.  Many of these
are straightforward.  For instance, see the TextWidget in textwidgets.py, which
is a subclass of BrowserWidget in widget.py.  It is registered as an
IBrowserRequest view of an ITextLine schema field, providing the IInputWidget
interface::

  <view
      type="zope.publisher.interfaces.browser.IBrowserRequest"
      for="zope.schema.interfaces.ITextLine"
      provides="zope.app.form.interfaces.IInputWidget"
      factory=".TextWidget"
      permission="zope.Public"
      />

The widget then receives the field and the request as arguments to the factory
(i.e., the TextWidget class).

Some widgets in Zope 3 extend this pattern.  This extension is configurable:
simply do not load the zope/app/form/browser/configure.zcml file if you do not
wish to participate in the extension.  The widget registration is extended for
Choice fields and for the collection fields.

Default Choice Field Widget Registration and Lookup
===================================================

As described above, all field widgets are obtained by looking up a browser
IInputWidget or IDisplayWidget view for the field object.  For Choice fields,
the default registered widget defers all of its behavior to the result of
another lookup: a browser widget view for the field *and* the Choice field's
vocabulary.  

This allows registration of Choice widgets that differ on the basis of the
vocabulary type.  For example, a widget for a vocabulary of images might have
a significantly different user interface than a widget for a vocabulary of
words.  A dynamic vocabulary might implement IIterableVocabulary if its
contents are below a certain length, but not implement the marker "iterable"
interface if the number of possible values is above the threshhold.

This also means that choice widget factories are called with with an additional
argument.  Rather than being called with the field and the request as
arguments, choice widgets receive the field, vocabulary, and request as
arguments.

Some Choice widgets may also need to provide a query interface, particularly if
the number of items in the vocabuary are too big to iterate.  The vocabulary
may provide a query which implements an interface appropriate for that
vocabulary.  You then can register a query view--a view registered for the
query interface and the field interface--that implements
zope.app.forms.browser.interfaces.IVocabularyQueryView.

Default Collection Field Widget Registration and Lookup
=======================================================

The default configured lookup for collection fields--List, Tuple, and Set, for
instance--begins with the usual lookup for a browser widget view for the
field object.  This widget defers its display to the result of another lookup:
a browser widget view registered for the field and the field's value_type (the
type of the contained values).  This allows registrations for collection
widgets that differ on the basis of the members--a widget for entering a list
of text strings might differ significantly from a widget for entering a list of
dates...or even a list of choices, as discussed below.

This registration pattern has three implications that should be highlighted. 

 * First, collection fields that do not specify a value_type probably cannot
   have a reasonable widget.  

 * Second, collection widgets that wish to be the default widget for a
   collection with any value_type should be registered for the collection
   field and a generic value_type: the IField interface.  Do  not register the
   generic widget for the collection field only or you will break the lookup
   behavior as described here.
 
 * Third, like choice widget factories, sequence widget factories (classes or
   functions) take three arguments.  Typical sequence widgets receive the
   field, the value_type, and the request as arguments.

Collections of Choices
----------------------

If a collection field's value_type is a Choice field, the second widget again
defers its behavior, this time to a third lookup based on the collection field
and the choice's vocabulary.  This means that a widget for a list of large
image choices can be different than a widget for a list of small image choices
(with a different vocabulary interface), different from a widget for a list of
keyword choices, and different from a set of keyword choices.

Some advanced applications may wish to do a further lookup on the basis of the
unique attribute of the collection field--perhaps looking up a named view with
a "unique" or "lenient" token depending on the field's value, but this is not
enabled in the default Zope 3 configuration.

Registering Widgets for a New Collection Field Type
---------------------------------------------------

Because of this lookup pattern, basic widget registrations for new field types
must follow a recipe.  For example, a developer may introduce a new Bag field
type for simple shopping cart functionality and wishes to add widgets for it
within the default Zope 3 collection widget registration.  The bag widgets
should be registered something like this. 

The only hard requirement is that the developer must register the bag + choice
widget: the widget is just the factory for the third dispatch as described
above, so the developer can use the already implemented widgets listed below.

  <view
      type="zope.publisher.interfaces.browser.IBrowserRequest"
      for="zope.schema.interfaces.IBag
           zope.schema.interfaces.IChoice"
      provides="zope.app.form.interfaces.IDisplayWidget"
      factory=".ChoiceCollectionDisplayWidget"
      permission="zope.Public"
      />

  <view
      type="zope.publisher.interfaces.browser.IBrowserRequest"
      for="zope.schema.interfaces.IBag
           zope.schema.interfaces.IChoice"
      provides="zope.app.form.interfaces.IInputWidget"
      factory=".ChoiceCollectionInputWidget"
      permission="zope.Public"
      />

Beyond this, the developer may also have a generic bag widget she wishes to
register.  This might look something like this, assuming there's a
BagSequenceWidget available in this package::

  <view
      type="zope.publisher.interfaces.browser.IBrowserRequest"
      for="zope.schema.interfaces.IBag
           zope.schema.interfaces.IField"
      provides="zope.app.form.interfaces.IInputWidget"
      factory=".BagSequenceWidget"
      permission="zope.Public"
      />

Then any widgets for the bag and a vocabulary would be registered according to
this general pattern, in which IIterableVocabulary would be the interface of
any appropriate vocabulary and BagWidget is some appropriate widget::

  <view
      type="zope.publisher.interfaces.browser.IBrowserRequest"
      for="zope.schema.interfaces.IBag
           zope.schema.interfaces.IIterableVocabulary"
      provides="zope.app.form.interfaces.IInputWidget"
      factory=".BagWidget"
      permission="zope.Public"
      />
-------------- next part --------------
======
Fields
======

This document highlights unusual and subtle aspects of various fields and
field classes, and is not intended to be a general introduction to schema
fields.  Please see README.txt for a more general introduction.

While many field types, such as Int, TextLine, Text, and Bool are relatively
straightforward, a few have some subtlety.  We will explore the general
class of collections and discuss how to create a custom creation field; discuss
Choice fields, vocabularies, and their use with collections; and close with a
look at the standard zope.app approach to using these fields to find views
("widgets").

Collections
===========

Normal fields typically describe the API of the attribute--does it behave as a
Python Int, or a Float, or a Bool--and various constraints to the model, such
as a maximum or minimum value.  Collection fields have additional requirements
because they contain other types, which may also be described and constrained.

For instance, imagine a list that contains non-negative floats and enforces
uniqueness, In a schema, this might be written as follows::

  >>> from zope.interface import Interface
  >>> from zope.schema import List, Float
  >>> class IInventoryItem(Interface):
  ...     pricePoints = List(
  ...         Float(title=u"Price", min=0),
  ...         unique=True, title=u"Price Points")

This indicates several things.

- pricePoints is an attribute of objects that implement IInventoryItem.
- The contents of pricePoints can be accessed and manipulated via a Python list
  API.
- Each member of pricePoints must be a non-negative float.
- Members cannot be duplicated within pricePoints: each must be must be unique.
- The attribute and its contents have descriptive titles.  Typically these
  would be message ids.

This declaration creates a field that implements a number of interfaces, among
them these::

  >>> from zope.schema.interfaces import IList, ISequence, ICollection
  >>> IList.providedBy(IInventoryItem.pricePoints)
  True
  >>> ISequence.providedBy(IInventoryItem.pricePoints)
  True
  >>> ICollection.providedBy(IInventoryItem.pricePoints)
  True

Creating a custom collection field
----------------------------------

Ideally, custom collection fields have interfaces that inherit appropriately
from either zope.schema.interfaces.ISequence or
zope.schema.interfaces.IUnorderedCollection.  Most collection fields should be
able to subclass zope.schema._field.AbstractCollection to get the necessary
behavior.  Notice the behavior of the Set field in zope.schema._field: this
would also be necessary to implement a Bag.

Choices and Vocabularies
========================

Choice fields are the schema way of spelling enumerated fields and more.  By
providing a dynamically generated vocabulary, the choices available to a
choice field can be contextually calculated.  

Simple choices do not have to explicitly use vocabularies::

  >>> from zope.schema import Choice
  >>> f = Choice((640, 1028, 1600))
  >>> f.validate(640)
  >>> f.validate(960)
  Traceback (most recent call last):
  ...
  ConstraintNotSatisfied: 960
  >>> f.validate('bing')
  Traceback (most recent call last):
  ...
  ConstraintNotSatisfied: bing

More complex choices will want to use registered vocabularies.  Vocabularies
have a simple interface, as defined in
zope.schema.interfaces.IBaseVocabulary.  A vocabulary must minimally be able
to determine whether it contains a value, to create a term object for a value,
and to return a query interface (or None) to find items in itself.  Term
objects are an abstraction that wraps a vocabulary value.  

The Zope application server typically needs a fuller interface that provides
"tokens" on its terms: ASCII values that have a one-to-one relationship to the
values when the vocabulary is asked to "getTermByToken".  If a vocabulary is
small, it can also support the IIterableVocabulary interface.

If a vocabulary has been registered, then the choice merely needs to pass the
vocabulary identifier to the "vocabulary" argument of the choice during
instantiation.

A start to a vocabulary implementation that may do all you need for many simple
tasks may be found in zope.schema.vocabulary.SimpleVocabulary.  Because
registered vocabularies are simply callables passed a context, many
registered vocabularies can simply be functions that rely on SimpleVocabulary::

  >>> from zope.schema.vocabulary import SimpleVocabulary
  >>> def myDynamicVocabulary(context):
  ...     v = dynamic_context_calculation_that_returns_an_iterable(context)
  ...     return SimpleVocabulary.fromValues(v)
  ... 

The vocabulary interface is simple enough that writing a custom vocabulary is
not too difficult itself.

Choices and Collections
-----------------------

Choices are a field type and can be used as a value_type for collections.  Just
as a collection of an "Int" value_type constrains members to integers, so a
choice-based value type constrains members to choices within the Choice's
vocabulary.  Typically in the Zope application server widgets are found not
only for the collection and the choice field but also for the vocabulary on
which the choice is based.

Using Choice and Collection Fields within a Widget Framework
============================================================

While fields support several use cases, including code documentation and data
decription and even casting, a significant use case influencing their design is
to support form generation--generating widgets for a field.  Choice and
collection fields are expected to be used within widget frameworks.  The
zope.app approach typically (but configurably) uses multiple dispatches to 
find widgets on the basis of various aspects of the fields.

Widgets for all fields are found by looking up a browser view of the field
providing an input or display widget view.  Typically there is only a single
"widget" registered for Choice fields.  When it is looked up, it performs
another dispatch--another lookup--for a widget registered for both the field
and the vocabulary.  This widget typically has enough information to render
without a third dispatch.

Collection fields may fire several dispatches.  The first is the usual lookup
by field.  A single "widget" should be registered for ICollection, which does
a second lookup by field and value_type constraint, if any, or, theoretically,
if value_type is None, renders some absolutely generic collection widget that
allows input of any value imaginable: a check-in of such a widget would be
unexpected.  This second lookup may find a widget that knows how to render,
and stop.  However, the value_type may be a choice, which will usually fire a
third dispatch: a search for a browser widget for the collection field, the
value_type field, and the vocabulary.  Further lookups may even be configured
on the basis of uniqueness and other constraints.

This level of indirection may be unnecessary for some applications, and can be
disabled with simple zcml changes within zope.app.


More information about the Zope3-dev mailing list