[Zope3-dev] Applying schema data on an object

Jeff Shell eucci.group at gmail.com
Wed Dec 13 23:32:26 EST 2006


On 12/13/06, Christian Theune <ct at gocept.com> wrote:
> Hi,
>
> I've been trying to DRY some code and found that the following pattern
> keeps popping up:
>
> class SomeObject(object):
>
>         def __init__(self, many_keyword_arguments):
>                 self.y = y
>                 self.x = x
>                 self.z = z
>
> From the outside this then is called from a view (where data comes from
> a formlib AddForm:
>
>         def create(self, **data)
>                 return SomeObject(**data)
>
> I'd like to remove the explicit assignments from the constructor and use
> a function to do this in a generic way. I didn't find anything that does
> this yet, except from a formlib function that requires FormFields
> instead of a schema, so I wrote my own:
>
> -----
> def set_schema_data(context, schema, data):
>     """Update the context's attributes with the given data that belongs
>        to the specified schema.
>     """
>     for name in schema:
>         field = schema[name]
>         if not zope.schema.interfaces.IField.providedBy(field):
>             continue
>         if name in data:
>             field.set(context, data[name])
> -----

I've used a similar pattern for a while - especially in a couple of
non-zope experiments that used interfaces and schema. I've noticed
that I seldom write initializers for my persistent objects anymore, as
I often have formlib or some other function applying data. That's
probably a bad habit.

One thing that I like about formlib is the ability to combine fields
from multiple Interfaces together, and have adaptation apply them.
Outside of form usage, however, this might not be as necessary. With
forms, it's excellent, as DublinCore fields (like 'title') can easily
be included in the field set.

There are more complex things you could do in here, such as validation
or setting of defaults. Even without that, here's what I would do::

    import zope.schema

    def applyschema(target, data, schema, validate=False, applydefaults=False):
        for name, field in zope.schema.getFieldsInOrder(schema):
            field = field.bind(target)

            if name not in data:
                if applydefaults:
                    field.set(target, field.default)
                continue

            value = data.get(name)
            if validate:
                field.validate(value)
            field.set(target, value)

> There is a function around already which comes from zope.formlib and
> requires the schema to be specified as a FormFields setup. I'd like to
> not have the formlib dependency in this code.
>
> I propose to include the given function in zope.schema or
> zope.interface. Eventually this could also be spelled as a method on a
> schema (symmetrical to field.set): schema.set(object, data)

Would that then be a function of Interfaces? zope.schema is basically
nothing but a bunch of extensions of `zope.interface.Attribute` and
some helper functions to filter out Field objects from the Interface's
members. If applied to zope.interface, you'd have to either move the
basic definition of a 'schema field' to zope.interface (which I
believe is beyond its scope), or have 'schema.set' work for Attributes
(but not interface.Method, which is also extensions of Attribute).

I'd propose putting this in `zope.schema._schema` or some other module
in zope.schema for utility functions. It could also include my
favorite trick - a schema-to-dict-by-way-of-tuple-pairs iterator!

    def __iter__(self):
        """ Yields pairs of (name, value) reflecting IStreetAddress """
        for name, field in zope.schema.getFieldsInOrder(IStreetAddress):
            yield (name, field.query(self, field.default))

Since ``dict(iterable)`` works if the iterable produces (key,value)
pairs, this is a handy to get consistent mappings out of objects. I
use the above heavily to copy / move address data around between ZODB,
RDBMS (via SQLAlchemy), and CSV files. Maybe made as a general
function like this? ::

    _marker = object()
    def pairs(source, schema, applydefaults=True):
        """
        Yields pairs of (name, value) from `source` based on the schema fields
        in 'schema'. If 'applydefaults' is True (default), missing values will
        be replaced with the default specified on the field.
        """
        for name, field in zope.schema.getFieldsInOrder(schema):
            value = field.query(source, _marker)
            if (value is _marker) and applydefaults:
                value = field.default
            yield (name, value)

    address = dict(pairs(contact.address, IStreetAddress))
    # or
    pprint.pprint(list(pairs(contact.address, IStreetAddress)))

that latter one is handy when debugging.

-- 
Jeff Shell


More information about the Zope3-dev mailing list