Registering Add-on-specific components using z3c.baseregistry
(Updated 10/17/2010 to reflect some minor corrections from Martin.)
At Groundwire it is very common for us to run a number of Plone sites within the same Zope instance. This introduces some unique requirements for add-on products to follow when they are registering Zope components:
- If the add-on registers some new component, it should do it in such a way that it is only available within sites that have the add-on installed.
- If the add-on overrides some default component from Zope or Plone, it should do it in such a way that it can be further overridden for one particular site.
There are several common approaches that can help with these requirements, but each has downsides. Overriding components using overrides.zcml is global (i.e., affects all Plone sites in an instance) and prevents further customization. Registering components for a browser layer only works for components that adapt the request (such as browser views). Registering persistent local utilities or adapters in a site's local component registry keeps things isolated, but can be a headache when it's time to uninstall the add-on or remove the implementation of a component.
There is a lesser-known fourth option: using z3c.baseregistry to create a registry specific to one add-on.
Component registries in Zope 2
In Zope 2 we typically deal with 2 types of component registries (also called site managers historically):
- The global registry, which is populated with components at startup by processing ZCML.
- A local registry associated with each Plone site (implemented in five.localsitemanager). These store components persistently in the ZODB, and can be populated via the
zope.component.interfaces.IComponentRegistrationAPI in Python, or via GenericSetup (componentregistry.xml)
When Zope traverses over a Plone site, its local registry is set as the active registry (via
zope.app.component.hooks.setSite in older Zopes—which sets a thread local). After that, this registry is the one that can be obtained by
zope.component.getSiteManager, and the one that will be implicitly used by the functions that do component lookups. If a component is looked up but not found in the local registry, it will fall back to checking in that registry's base registries. By default in Plone, there is just one base registry, which is the global registry.
However, there's no requirement that the global registry be the only base registry. z3c.baseregistry makes it possible to define additional, named registries which can be installed as additional base registries for a particular site. Then when a component lookup occurs, it will be looked for first in the local registry, then in the custom base registry, and then in the global registry.
The cool thing is that while the installation of a z3c.baseregistry is persistent, the components one contains are not. Instead, the components are populated at Zope startup via ZCML, very much like the global registry. The
registerIn grouping directive lets us specify which registry components should be registered in:
<registerIn registry=".packageComponents"> <!-- component directives here --> </registerIn>
This means that when you uninstall an add-on that has its own base registry, you just need to remove the registry from the site manager's bases, rather than figuring out how to unregister each individual component as would be necessary for persistent components in the local registry. It also means that you can safely remove a component's class when you remove its registration without worrying about breaking legacy persistent registrations of that component.
Step by step
I've used this approach in a few projects lately. Here's what it looks like:
- Add z3c.baseregistry to the add-on's install_requires in setup.py, and re-run buildout to make sure it is installed.
Create a new registry instance.
In __init__.py (or could be elsewhere):
from zope.component import getGlobalSiteManager from z3c.baseregistry.baseregistry import BaseComponents packageComponents = BaseComponents(getGlobalSiteManager(), 'foo.bar')
Here, we made sure that the new registry has the global registry as its base, and is named after our add-on package (foo.bar).
Register a local utility for looking up the new registry by name (this is used by z3c.baseregistry internally).
<!-- registry for package-specific components --> <utility component=".packageComponents" provides="zope.component.interfaces.IComponents" name="foo.bar" />
Install the new registry in the bases for a particular site. z3c.baseregistry includes a form for doing this through the web, but it doesn't seem to work in Zope 2. Oh well, we can do it with a GenericSetup import handler instead.
from zope.component import getSiteManager from zope.component.interfaces import IComponents def install_base_registry(site): sm = getSiteManager(context=site) reg = sm.getUtility(IComponents, name=u'foo.bar') sm.__bases__ = tuple([reg] + [r for r in sm.__bases__ if r is not reg])
You would then call this from the add-on's custom "import various" GenericSetup handler.
Now components can be registered for the new add-on specific registry, using the registerIn grouping directive.
<!-- make sure we can use registerIn --> <include package="z3c.baseregistry" file="meta.zcml"/> <registerIn registry=".packageComponents"> <browser:page for="*" name="foobar" template="foobar.pt" permission="zope.Public" /> </registerIn>
These components will be found within sites that have the product installed, but not within sites that don't!
- It should be obvious, but this only localizes the effects of ZCML directives whose effect is to register something in the component registry (e.g. utility, adapter, subscriber, browser:page). Directives that mutate other things, such as
<class>which directly modifies a class, will still have a global effect.
- Don't forget to make sure that the base registry gets removed from the local registry's bases when the add-on is uninstalled. Otherwise removing the product will break the site when it tries to unpickle the base registry.