Another long post, where below I take apart
zope.formlib.namedtemplate, hoping it can be used as a source of
insight into how to provide flexible extensibility (like in this
example AnnotationsAdapter concept) without needing
yet-another-ZCML-directive.

On 3/15/06, Martijn Faassen <[EMAIL PROTECTED]> wrote:
> Jim Fulton wrote:
> > This may not be documented as well as it should be, but not for lack of
> > trying.
> >
> > 1. You can always provide the interface provided by an adapter
> >    when you register it.
>
> I realize this, but I was trying to implement the example as suggested,
> also because it's clear that the tendency is away from specifying 'for'
> and 'implements' in ZCML. I know I wouldn't have a problem if I use
> 'for' and 'implements' in ZCML.

I don't think there's anything wrong with specifying 'for' and
'implements' in ZCML. It serves a couple of purposes:

1. It is where you can override \ augment an existing configuration.
2. It's where you can specify those arguments for objects and situations
   that aren't easy to specify in Python.

I prefer to see 'implements' and 'adapts' in Python code as much as
possible, but there are certainly situations where specifying it in
ZCML is fine. It's also allowable to provide those arguments in
zope.component.provideAdapter / zapi.provideAdapter.

> > 2. If a factory declares that it implements a single interface,
> >    then you can omit the interface when registering the adapter.
> >
> >    If a factory is a class, it typically declares that it implements
> >    an interface via the implements call in the class statement.
>
> Well, I found this out by reading the code in zope.app.component.
>
> >    If a factory is not a class, and if it allows attributes to be
> >    set on it, then the interface.implementor function can be used to make
> >    declarations for it.  This is documeted in zope/interfaces/README.txt
> >    and zope/component/README.txt.
>
> This is one bit I was missing, thanks.
>
> Unfortunately I read in zope/interface/README.txt that the 'implementer'
> function cannot be used for classes yet, so this will change the design
> somewhat (I was using __call__, looks like I'll have to exploit lexical
> scoping and generate a function on the fly).
>
> Using implements() on the class seems like lying, as the class is just
> implementing a factory, not the functionality itself. Besides, I'd have
> to change the class each time it gets called (it's using implementedBy
> to check).

When you say 'implements() on the class', do you mean::

    class FooAdapter(...):
        implements(IFoo)

I believe that the interfaces documentation says that this statement
means 'instances of this class provide the IFoo interface'.
zope.interface.classProvides is what you use to make declarations
about the class itself.

    class FooAdapter(...):
        implements(IFoo)
        classProvides(IFooFactory)

> Then that leaves convincing 'adaptedBy()'. One hack is to write the
> 'for_' argument that's passed to the AnnotationAdapter() constructor
> directly to an attribute called __component_adapts__. I don't think I
> can use 'adapts()' on a lexically scoped function...
>
> I stand by my conclusions on this approach sounding simple in theory,
> but still being a bit harder than it should be in practice. :)

I feel the same way. I may have missed some obvious piece of
documentation. Or maybe it's just not an obvious piece of
documentation. But there do seem to be certain rules about what can
register as an adapter. I believe this is why so many of the browser:
directives generate new classes dynamically.

There's a good possibility that you may be over-complicating the
situation. Since I haven't tried implementing my suggestion myself (it
was mostly wishful thinking on what I'd prefer to see), I can't say
for sure. But NamedTemplates in formlib seem to be doing something
similar... I think.

In zope/formlib/form.py::

    class Action(...)
        implements(interfaces.IAction)
        ...
        render = namedtemplate.NamedTemplate('render')

    @namedtemplate.implementation(interfaces.IAction)
    def render_submit_button(self):
        if not self.available():
            return ''
        ...

In zope/formlib/configure.zcml::

    <adapter factory=".form.render_submit_button" name="render" />

The implementation of named templates (zope/formlib/namedtemplate.py)
looks rather small. It uses zope.interface.implementer and
zope.component.adapts. It's probably a good example of .... something.
It's taken me a little while to figure it out from staring at it, but
I think I finally get it now. I'll document my interpretation of it.
Anyone's free to correct me if I'm wrong. I think this may help in
providing ways of keeping the number of ZCML directives down.

Looking at ``render = namedtemplate.NamedTemplate('render')``. That
creates an instance of this class::

    class NamedTemplate(object):

        def __init__(self, name):
            self.__name__ = name

        def __get__(self, instance, type=None):
            if instance is None:
                return self
            return component.getAdapter(instance, INamedTemplate,
                self.__name__)

        def __call__(self, instance, *args, **kw):
            self.__get__(instance)(*args, **kw)

I don't think this has any benefits to an Annotations helper, but I
thought it would help to start at the source. Basically, the Action
class has an attribute, 'render', that when used on an instance will
get an adapter with the name 'render' that provides INamedTemplate for
the action instance. The __call__ method here allows it to be used in
situations like: ``Action.render(action, ...)`` (going through the
class instead of the instance to call the thing).

Next we have [EMAIL PROTECTED](interfaces.IAction)``
which decorates (in this case) a function. It uses the following
class::

    class implementation:

        def __init__(self, view_type=None):
            self.view_type = view_type

        def __call__(self, descriptor):
            return NamedTemplateImplementation(descriptor, self.view_type)

It's basically doing ``render_submit_button =
NamedTemplateImplementation(render_submit_button,
interfaces.IAction)``. This is where things get interesting::

    class NamedTemplateImplementation:

        def __init__(self, descriptor, view_type=None):
            try:
                descriptor.__get__
            except AttributeError:
                raise TypeError(
                    "NamedTemplateImplementation must be passed a descriptor."
                    )
            self.descriptor = descriptor
            interface.implementer(INamedTemplate)(self)

            if view_type is not None:
                component.adapter(view_type)(self)

        def __call__(self, instance):
            return self.descriptor.__get__(instance, instance.__class__)

In __init__, this particular instance calls interface.implementer and
component.adapter on itself, which ultimately sets up the attributes
``__implemented__`` and ``__component_adapts__``. And its this
instance that gets registered as an adapter factory, like::

    <adapter factory=".form.render_submit_button" name="render" />

So now, ``.form.render_submit_button`` is a callable object (an
instance) that has the __implemented__ and __component_adapts__
attributes set. The ZCML/zope.component machinery can see that and
figure out that it can call ``render_submit_button`` to get the
INamedTemplate adapter named 'render' for all IAction objects.

**NamedTemplateImplementation instances are adapter factories.**  If
'instance' in the __call__ were 'context', it might have been a bit
more obvious that this is what was going on.

But maybe this helps?

This NamedTemplate stuff isn't exactly clear, and it is a bit magical
on its own. Some of the special magical-looking parts have to do with
making NamedTemplates be attributes (so that the action class can call
'self.render()'). Don't let that aspect get in the way of what can be
learned here. This is a feature that allows specialized
views/renderings to be provided. And it accomplishes all of this:

* No new ZCML Directives - only adapters are registered. formlib's
  configure.zcml registers three adapters that provide INamedTemplate - one
  for rendering IAction implementations, two for forms (one for full page
  forms, one for sub-forms). It registers one other adapter to provide a TALES
  namespace. And that's it. For such a comprehensive and extensible package,
  that's awfully nice and light on the ZCML.

* Works with both ViewPageTemplateFiles and functions.

* Using overrides or sub-interfaces, you can provide other implementations
  (I'm using some base interfaces for an application I have now, based on
  IForm, with a replacement form template provided), without having an impact
  on the base software.

* In setting up test harnesses or other situations where ZCML may not be
  loaded / loadable, it should be a little more obvious that
  provideAdapter(...) should work as expected. This is not obvious in
  many situations where a ZCML directive like 'vocabulary', 'renderer', and
  more are nice masks over provideAdapter()/provideUtility() - but those
  masks are unavailable outside of the ZCML run time (leaving you to your
  own devices to figure out how to get a vocabulary registered in just
  the right way to test a zope.schema.Choice field).

* You can provide INamedTemplate implementations in the more classic way if
  you'd like::

      class CancelButton(object):
          implements(INamedTemplate)
          adapts(ICancelAction)

          def __init__(self, context):
              self.context = context

          def __call__(self):
              if not self.context.available():
                  return u''
              ...

      <adapter factory=".CancelButton" name="render" />

That last bullet is important. This is the "yeah, but how do I grow up
beyond ___?" answer. With custom ZCML directives, there often comes a
point where you hit a wall where you need to do something different
than what the directive allows. NamedTemplateImplementation does a lot
of what the specialized directives do: automation of a common
registration. But by NOT having it as a custom directive, hopefully it
makes it just a little more obvious that you can actually do an
INamedTemplate implementation of your own and it would work exactly
the same. (Not that the namedtemplate.py code goes out of its way to
make this clear, but it is still easier to understand than the
discriminator/handler stuff of ZCML).

--
Jeff Shell
_______________________________________________
Zope3-dev mailing list
Zope3-dev@zope.org
Unsub: http://mail.zope.org/mailman/options/zope3-dev/archive%40mail-archive.com

Reply via email to