Grok (and martian) rocks
Even if you're writing Plone code
The fine folks over at the Grok project have taken it upon themselves to release the basic components of the Grok framework - those that "grok" your package for components and things to register - so that they can be used in other systems. The library that supports writing grokkers is called Martian. A package called grokcore.component contains the wiring for using Martian in Zope 2 land.
For grokcore.component to be able to grok your package, you need to have the following in your configure.zcml:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:grok="http://namespaces.zope.org/grok"> <grok:grok package="." /> </configure>
The idea is that you shouldn't need too much else in you configure.zcml after this if your code is self-describing. Here is an example of an adapter registration, for example:
import grokcore.component class MyAdapter(grokcore.component.Adapter): grokcore.component.context(IAdaptedContext) grokcore.component.implements(IMyInterface) # No need to implement __init__, it's already # provided by the base class. def do_something(self): pass
When the grokker sees this, it knows that is is an adapter (because of the base class) and it knows that it adapts a context of type IAdaptedContext and provide an interface IMyInterface. There is no need for a separate ZCML line to tell the Component Architecture about this component.
grokcore.component provides grokkers for the basic components, like adapters, utilities and subscribers. It works in Zope 3 as well as in Zope 2. All you have to do is depend on it, make sure that its meta.zcml file is loaded, and use it as above.
Writing custom grokkers
What got me really excited, though, is how easy it is to write your own grokkers. For example, in Dexterity we wanted to be able to initialise a class with fields declared in a schema interface if those were not already provided by the class. The syntax we were aiming for was:
from zope.interface import implements, Interface from zope import schema from plone.dexterity import api class IMyType(api.Schema): title = schema.TextLine(title=u"Title") body = schema.Text(title=u"Body text", required=False, default=u"Body text goes here") class MyType(api.Item): implements(IMyType) api.meta_type("PyPage") api.add_permission("cmf.AddPortalContent")
After grokking, MyType should be registered as a Zope 2 content class (in the same way that the <five:registerClass /> ZCML directive would do) with the given meta type and add permission, and it should have the attributes 'title' and 'body', even if these did not exist already, initialised to default values.
The grokker looks like this:
import martian from zope.interface import implementedBy from zope.schema import getFieldsInOrder from plone.supermodel.directives import Schema from plone.dexterity.content import DexterityContent from Products.Five.fiveconfigure import registerClass class meta_type(martian.Directive): """Directive used to specify the meta type of an object """ scope = martian.CLASS store = martian.ONCE default = None validate = martian.validateText def factory(self, meta_type): return meta_type class add_permission(martian.Directive): """Directive used to specify the add permission of an object """ scope = martian.CLASS store = martian.ONCE default = u"cmf.AddPortalContent" validate = martian.validateText def factory(self, permission): return permission class ContentGrokker(martian.ClassGrokker): martian.component(DexterityContent) martian.directive(meta_type) martian.directive(add_permission) def execute(self, class_, config, meta_type, add_permission, **kw): # 1. Register class if a meta type was specified. Most types # will probably not need this. if meta_type is not None: registerClass(config, class_, meta_type, add_permission) # 2. Initialise properties from schema to their default values if # they are not already on the class. for iface in implementedBy(class_).flattened(): if iface.extends(Schema): for name, field in getFieldsInOrder(iface): if not hasattr(class_, name): setattr(class_, name, field.default) return True
We first define to two directives, meta_type() and add_permission(). Martian has base classes to make this easy.
We then declare the grokker itself, specifying that it should work on classes deriving from DexterityContent, and that it supports the aforementioned meta_type() and add_permission() directives. When the class is grokked (at ZCML parse time), the execute() method is called, and is passed the class we grokked, the zope.configuration config object, and the value of our two directives. We then do our work and return True if the class was successfully grokked.
For this to work, we need to Grok the package providing the custom grokker (grokkit?). That is done in a meta.zcml file for plone.dexterity, which looks like this:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:grok="http://namespaces.zope.org/grok" i18n_domain="plone.dexterity"> <!-- Load ZCML from Grok and grok our grokkers --> <include package="grokcore.component" file="meta.zcml" /> <grok:grok package=".directives" /> </configure>
Martian has decent documentation on its PyPI page if you want to learn more.
Obviously, Martian is a tool for framework authors. Most people will never write a grokker. But I'm really excited about how easy it is to write them. There is a definite risk of over-use leading to too many things happening implicitly, but used wisely, the patterns that Grok has espoused could help make Plone easier to code for.
Some grokkers that I'd like to see include:
- Views with and without templates (mostly done, I think)
- Viewlets (ditto)
- Portlets (ditto)
- Archetypes content classes (to facilitate the type registration and setting security)