[Zope3-dev] Riddle: zope.interface versus zope.formlib

Jeff Shell eucci.group at gmail.com
Fri Aug 25 15:17:41 EDT 2006


On 8/23/06, Stephan Richter <srichter at cosmos.phy.tufts.edu> wrote:
> Hi everyone,
>
> today I found a nice brain teaser. What is the outcome of the following:
>
> >>> from zope.interface import Interface
> >>> from zope.schema import TextLine
> >>> class IFoo(Interface):
> ...     title = TextLine()
> ...
> >>> class IBar(IFoo):
> ...     pass
> ...
> >>> IBar['title']
> <zope.schema._bootstrapfields.TextLine object at 0xb7bc17ac>
> >>> IBar['title'].interface
> ???????????????????

I now know this answer since I came across this a while ago: IFoo!

Someone mentioned classes. This is not much different than this simple
situation:

    >>> class Foo(object):
    ...     title = 'bar'
    >>> class Bar(Foo):
    ...     pass
    >>> Bar.title
    'bar'
    >>> 'title' in Bar.__dict__
    False
    >>> 'title' in Foo.__dict__
    True

> If you ask the zope.interface.interfaces.IAttribute documentation you get:
>
>     interface = Attribute('interface',
>                           'Stores the interface instance in which the '
>                           'attribute is located.')
>
> In plain English: <InterfaceClass __main__.IFoo>
>
> If you ask zope.formlib (form.py, line 227)::
>
>   # Adapt context, if necessary
>   interface = field.interface
>   ...
>   adapter = interface(context)
>
> Here the answer would be: <InterfaceClass __main__.IBar>

Could you clarify 'would be'? Do you mean 'the answer that I often
find myself wanting' or 'the answer IS'?

If the anser IS ...IBar, it's wrong. Unless something has changed
since Zope 3.2.

Actually, what exactly are you asking here? Is the question `what is
the interface that should be provided by ``adapter``? Or is the
question `what is the value of `interface`?

    >>> class Bar(object):
    ...     implements(IBar)
    ...     title = u""
    >>> bar = Bar()
    >>> IBar['title'].interface
    <InterfaceClass __main__.IFoo>
    >>> IBar['title'].interface(bar)
    <__main__.Bar object at 0x2907630>

Isn't that what formlib is doing? How is this wrong?

> Either way you look at it, one package is wrong and thus broken.

I'm not sure if anything is wrong and broken, besides concepts,
clarity, and terminology.

Personally, I would really love a way to re-combine Interface Fields.
I like how formlib.FormFields can be composed from many sources, or
from a single source selectively, and I wanted this at the Interface
specification level.

I ran into this situation a couple of months ago when an
object-creation situation was getting very complex. The rules that
governed the main interface fields were ones that I wanted to reuse,
but selectively. I wanted to have this creation schema in its own
interface spec, since it was going to be combined (in theory) with
others. I basically wanted a Memo object that was very similar to this
complex set of objects I had to build, which could be passed around as
a single entity in comparison to having to add more and more arguments
(parameters?) to a Factory object every time we added something new to
the system. As the Memo was starting to build on other parts of the
system, it was becoming very hard to maintain and also to visualize.
We'd either have to copy and paste a lot of Interface specification
parts manually and maintain both, or look at some other options.

I ended up writing an interface cloner tool that would make a new
InterfaceClass dynamically, composing it of attributes selectively
copied out of other Interfaces. Then the 'interface' attribute could
be set on the copied Attribute without affecting the original.

    >>> import icopy
    >>> IBaz = icopy.iclone('IBaz', IBar)
    >>> IBaz
    <InterfaceClass __main__.IBaz>
    >>> IBaz['title']
    <zope.schema._bootstrapfields.TextLine object at 0x2a89630>
    >>> IBaz['title'].interface
    <InterfaceClass __main__.IBaz>
    >>> IBaz['title'].interface(bar)
    Traceback (most recent call last):
    ...
    TypeError: ('Could not adapt', <__main__.Bar object at 0x2907630>,
<InterfaceClass __main__.IBaz>)

Without copying, you end up with this scenario::

    >>> class IBadBar(IFoo):
    ...     title = IFoo['title']
    >>> IBadBar['title'].interface
    <InterfaceClass __main__.IBadBar>
    >>> IFoo['title'].interface
    <InterfaceClass __main__.IBadBar>

Whoops!

The copy tools also let you omit certain names, select certain names, combine
multiple interfaces, and so on. The problem was that - particularly
for this add form - I wanted to bring in bits of schema from other
interfaces

While I still have these tools around, I ultimately abandoned that
path after a day or two of work. Eventually we just manually built up
the FormField using the bits and pieces that we wanted.

> But which is it? It turns out that zope.interface wins and IFoo is returned.
> This really hurts zope.formlib. After an initial discussion on IRC, more
> people expected IBar, since it is more in line with im_class, but there were
> also people expecting IFoo.

How exactly does this hurt formlib?

I must say, the scenarios in `formlib` for how values get bound to
their fields is pretty confusing, between all of the different
`setUp...Widgets` scenarios and the adaptation. I got mad as hell
recently because I had a nearly impossible time getting today's date
to render for an edit-field situation ('get_rendered' couldn't be used
at that point). This was for a formlib based search form that kept the
criteria stored in the session. Fortunately, I had written an __iter__
on my Criteria class to turn the values into a dictionary (using
zope.schema!).

    def setUpWidgets(view, ignore_request=False):
        """
        Set up widgets and manually force `rangeStart` and `rangeEnd` because
        this is the only way to get defaults AND contextual values working in
        harmony.
        """
        context = view.criteria
        data = dict(context)
        data.update({
            'rangeStart': context.rangeStart or today(view),
            'rangeEnd': context.rangeEnd or threeMonths(view),
        })

        view.widgets = form.setUpWidgets(
            view.form_fields, context=context, form=view,
            data=data, ignore_request=ignore_request
        )

I had to do an insane amount of work to get to this point, as I poured
over all of the different formlib setUpWidgets data binding scenarios.
This is what hurts formlib. I think that the interface binding
scenario is only one small issue.

I think that tools to clone/copy/detach/monkey with schema Fields
would be prudent. A common scenario that I have is wanting to turn off
'required' for a certain situation. I want to use the same Schema
Field, but if the user doesn't enter a value, I want to compute one.
With the Interface-as-contract, that Field is required to exist. But
with the Interface-as-helping-render-this-form, I don't want to
require the user to enter something. This is another tricky situation,
especially if the Schema field has a lot of other settings on it.

Here's how I envision solving this.

    form_fields = form.Fields(
        ...
        form.Field(
            IEventSchema['start_date'].options(description='MM/DD/YYYY'),
        ),
        form.Field(
            IEventSchema['end_date'].options(required=False),
            custom_widget=...
        ),
        form.Field(
            # A copy of 'title', detached from its interface.
            IEventSchema['title'].options(interface=None),
        ),
    )

In the above scenario, the 'start_date' field keeps everything in the
normal schema field, but changes the description. 'end_date' isn't
required. If the user doesn't enter one, the software will compute a
value and store it on the target object, thus satisfying the default
interface 'required' contract. 'title' detaches itself from its
interface, preventing it from being forced to adapt.

Some of the core SQLAlchemy ORM units - Mapper and Query objects -
have this concept. Calling ``myquery =
query.options(lazyload('books'))`` returns a *copy* of the Query
object with all of its properties and configuration just like the
original, with modifications applied based on what's passed in to the
'options' call. This has been a wonderful and very powerful tool to
use, as some of the mappings can grow to be quite complex but some
usages won't require related objects to be loaded.

I think that zope.interface and/or zope.schema could benefit from this
scheme. Field.bind(context) already makes a clone of the interface
field; this theme could be expanded upon pretty easily::

    >>> titlebar = IBar['title'].bind(bar)
    >>> titlebar.required
    True
    >>> titlebar.required = False
    >>> titlebar.required
    False

    # Doesn't impact IBar['title']!

    >>> IBar['title'].required
    True
    >>> titlebar.interface
    <InterfaceClass __main__.IFoo>

    # Rebinding titlebar's interface
    >>> titlebar.interface = IBar
    >>> titlebar.interface
    <InterfaceClass __main__.IBar>
    >>> IBar['title'].interface
    <InterfaceClass __main__.IFoo>

But it would be nice to have a tool to make an altered copy in inline
expressions. It might also be useful to have a tool like 'getFieldIn'

    def getFieldIn(iface, name):
        """
        Gets the field by name out of the Interface `iface`. If the found
        field's `interface` value is different from `iface`, but `iface` is
        derived from the field's interface, the returned field will appear
        to come from the requested interface. Clear?

            ... (docstring examples of IFoo / IBar, showing how)
                ``getFieldIn(IBar, 'title')`` returns a new Field instance
                whose .interface value is `IBar`
        """
        # Cheap cloning trick
        field = iface[name].bind(None)

        # IBar extends IFoo, therefor IBar['title']
        if field.interface.isEqualOrExtendedBy(iface):
            field.interface = iface
        return field

Then when establishing fields in formlib.FormFields, one could use
this on single items or en masse. Maybe `form.Fields` could have a
constructor option of 'normalize=(*ifaces)', which will downcast
items. Or maybe 'getFieldIn' could take Field objects as well as
names. Or ...

> Before playing with it, I would like to know what other people
> think. So, what do you think?

I think that you haven't explained the situation adequately. I
understand what you're saying: "Oh, suddenly I wanted/expected
IBar['title'] to be bound to IBar when really it was bound to IFoo,"
but I don't understand the solution that you're trying to get to and
why. Specifically, I don't get how it "hurts formlib."

I think a good copy-and-modify tool for zope.interface.Attribute (or
at least zope.schema.Field) objects would be a more useful tool. I
think it should be part of the Field interface so that complex fields
(like the vocabulary-using ones) can do extra work if required.

And I think that some more clarity about how adaptation is used in
formlib's different setUp*Widgets and applyChanges methods might be a
good thing to have. I really like the pretty-much-automatic adaptation
that goes on, most of the time, but I find myself having to mind-trace
the source quite a bit just to remember the impact of different
options, parameters, or methods.

I think it would also be nice if it were easier to register/use
one-shot adapters that are made for just one form (I've done this in
the past, and it feels like a waste of typing and time and effort to
go through all the joys of ZCML registration and naming and thought
just to use a particular Adapter class/factory once - especially if
I'm doing the adaptation for just two fields out of, say, eight).

-- 
Jeff Shell


More information about the Zope3-dev mailing list