[Zope3-Users] Dynamically generate interfaces, content object and z3c.forms?
Martin Aspeli
optilude at gmx.net
Fri Aug 22 05:51:18 EDT 2008
Hi Harald,
> I need some advise how to use dynamically generated interfaces, content
> and forms:
I'm working on a package called Dexterity for CMF/Plone that can work
with content types created through-the-web (among other things). For TTW
content to have a unique interface, you need a dynamic one.
See http://martinaspeli.net/articles/dexterity for some more
information. This is slightly out of date, but not too much.
If you just want an interface at runtime, with no persistence to the
ZODB, then it's pretty easy. You can generate an interface (it helped me
a lot to understand that an interface, whilst defined with the 'class'
keyword, is really just an object of type InterfaceClass) and use it
however you want. It gets a bit more tricky if you want to have content
objects that are both persistent to the ZODB and provide the interface
you generate forevermore. However, it's not too difficult.
> 1. step:
> generate interfaces from xsd
> I've used zope.interface.interface.InterfaceClass to achieve this
You may be interested to look at
http://svn.plone.org/svn/plone/plone.supermodel which does the same
thing from an XML representation of zope.schema fields.
plone.supermodel will work in "plone Zope 3", so you should be able to
re-use it. I'd be really interested to see your XSD parsing code, by the
way! We thought about using XSD for Dexterity, although I find XSD
syntax a bit verbose.
Is it feasible to actually write the interface in Python?
plone.supermodel has a grokker (you don't need all of Grok, just the
re-usable grok.component) that lets you write something like this:
>>> class IMyType(plone.supermodel.Schema):
... plone.supermodel.model('mytype.xml')
plone.supermodel.Schema is just a marker interface.
At ZCML processing time, this will read 'mytype.xml' and scribble
zope.schema type fields onto IMyType. This interface is then every bit
as real as any other interface you have, and is quite easy to work with.
For inspiration:
http://dev.plone.org/collective/browser/example.dexterity/trunk/example/dexterity/page.py
If it's not feasible to write a per-file schema interface like this,
then you need to generate it all on the fly. That's easy too - you just
construct an InterfaceClass object at runtime and return it
(plone.supermodel has methods that do this, for example). The challenge,
as you've seen, is what you do with that once you have it ...
> 2. step:
> generate content object implementing the generated interface from step 1
> zope.interface.directlyProvides or classImplements?
> FieldProperty?
If you really want to make sure that
ISomeGeneratedSchema.providedBy(some_content) is True, then here's how
Dexterity does it:
http://dev.plone.org/plone/browser/plone.dexterity/trunk/plone/dexterity/factory.py
This is a custom IFactory utility (we register local utilities for each
TTW-defined content type). Your code doesn't need to be in such a
utility, of course. The relevant bits are:
1. Find a class to use. We store this in a string property that's TTW
editable and resolve it.
2. Create an instance of the class.
3. Look up the (dynamic) schema marker interface.
4. Check if the instance already provides the interface.
5. If not, do an alsoProvide() to mark the instance.
At this point, if you have a generic class, you've probably just told a
lie. Your generic class won't have the fields that the interface
promises. You can either go through and set properties on the instance,
or use a __getattr__() that can return field defaults. We opted for the
latter, since it makes it easier to deal with interfaces that change
over time. This is done here:
http://dev.plone.org/plone/browser/plone.dexterity/trunk/plone/dexterity/content.py
Here, we look up the type's schema again. Note that we need some kind of
registry to know which schema the instance is supposed to have.
Dexterity has all that in a utility, but you could just as easily have
done something like queryType(self).
We *did* look at other ways, such as generating a class for each
interface, or doing a dynamic __provides__ property. However, that got
very complicated, or slow, or both. The pattern above is pretty
straightforward.
However... :)
I assume you want to persist things in the ZODB as well? That gets a
little tricky, because if you use alsoProvides() to set it on an
instance, you need to ensure the interface has a real, stable module
path so that the ZODB can load it again.
The way we've solved it in Dexterity is this:
- We use a small package called plone.alterego
(http://svn.plone.org/svn/plone/plone.alterego/trunk/) that has some
hooks to let you create a dynamic module. You can re-use this in plain
Zope 3.
- The dynamic module has a __getattr__ hook which delegates to a
utility that you write. Your utility creates and returns the module's
object for that name.
- You need to be careful that your utility always returns the exact
same object, with the same python id(), otherwise you'll get stuck.
The utility in this case is SchemaModuleFactory in this file:
http://dev.plone.org/plone/browser/plone.dexterity/trunk/plone/dexterity/schema.py
The dynamic module 'generated' is kept there as well. When anything
tries to get any attribute from this, the SchemaModuleFactory utility
will be called.
This uses the interface name to determine what schema to load. You need
some kind of naming convention like that, since you won't know anything
else than the name that was accessed at this point.
Presuming you can look up the schema from the name, you just load it and
return it. Once you're sure it's not going to change, you can setattr()
it on the module so that the utility doesn't get called again.
However, you have to be able to deal with the case where the ZODB is
trying to load the object before you've got enough context to find the
interface's fields. In this case, we create a placeholder interface
which is then updated. Note that you can't just return a blank interface
and then return a different one later. plone.supermodel has a helper
method sync_schema() that can make one interface get the fields, bases
and tagged values of another. We use this to update the placeholder
interface with the real fields from the 'contextless' interface that
plone.supermodel returns when we load it.
- To actually get hold of this interface in the first place, you use
something like:
>>> from plone.dexterity.schema import generated
>>> my_schema = getattr(generated, 'my_schema', None)
This will either return the interface quickly if it has been fully
loaded and setattr'd onto the dynamic module, or it will invoke the utility.
> 3. step:
> generate z3c.forms with subforms(depending on interface complexity)
> There are 3 different types of forms in my use chase:
> a) simple interface, with simple attributes
> b) interface with zope.schema.Object to display
> c) interface with zope.schema.Dict and zope.schema.Object as value_type
> d) interface with zope.schema.List and zope.schema.Object as value_type
>
> but how?
> I've looked at the addressbook example in z3c.form but ...
> I'm still missing infos/knowledge about how to connect the three steps
This part is now quite easy. You need to make sure your views are
registered properly. One way is to have a generic view registered for
the common class. If you have a concrete interface, then it's easy to
register views for it. And if you have a dynamic interface, you can
register local components for this dynamic interface.
Once you have the view, it's all just business as usual with z3c.form.
Dexterity's forms are a bit complex because they also allow people to
specify UI hints in the schema. plone.supermodel parses this (via some
plugin utilities for application-specific logic) and stores them in
tagged values on the interface object. These are then used to set up the
form. There's also support for doing groups with objects rather than
classes. I suspect you won't need all of that to begin with though.
The forms are here:
http://dev.plone.org/plone/browser/plone.dexterity/trunk/plone/dexterity/browser/base.py
http://dev.plone.org/plone/browser/plone.dexterity/trunk/plone/dexterity/browser/edit.py
Hope this helps!
Martin
--
Author of `Professional Plone Development`, a book for developers who
want to work with Plone. See http://martinaspeli.net/plone-book
More information about the Zope3-users
mailing list