[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