I just tried to create a plugin system as per André Roberge's specification using the Zope Component Architecture. The program which is being made pluggable is a program written by the effbot. I'm sure the effbot will be thrilled. ;-)
This is not exactly the kind of problem domain where you'd expect to need plugins. It's a parser and needs to operate very quickly, and most plugin architectures (including the ZCA) are based on indirections that aren't really optimized to be inside the inner loop of programs that require very high speed. But that said, I suppose it's as good an application as any to introduce plugins into as a demonstration, as long as you're not judging on speed difference between the pluggable version and the original. I didn't measure speed, as I'd never use the component architecture "for real" in this particular program.
After completing pluginizing the application, a few things strike me. First of all, in order to get the ZCA to parse ZCML, it needs a lot of dependencies. We knew this, of course, but it's pretty striking exactly how many are required when you're dealing with this simple of a problem. Here they are:
pytz-2008i-py2.4.egg zope.component-3.4.0-py2.4.egg zope.configuration-3.4.0-py2.4.egg zope.deferredimport-3.4.0-py2.4.egg zope.deprecation-3.4.0-py2.4.egg zope.event-3.4.0-py2.4.egg zope.exceptions-3.5.2-py2.4.egg zope.i18n-3.6.0-py2.4.egg zope.i18nmessageid-3.4.3-py2.4-macosx-10.5-i386.egg zope.interface-3.4.1-py2.4-macosx-10.5-i386.egg zope.location-3.4.0-py2.4.egg zope.proxy-3.4.1-py2.4-macosx-10.5-i386.egg zope.proxy-3.4.2-py2.4-macosx-10.5-i386.egg zope.publisher-3.5.4-py2.4.egg zope.schema-3.4.0-py2.4.egg zope.security-3.5.2-py2.4-macosx-10.5-i386.egg zope.testing-3.5.1-py2.4.egg zope.traversing-3.5.0a4-py2.4.egg
That's just absurd. The publisher? zope.security?
zope.location? We really need to detangle these dependencies this
at some point to make it reasonable for very small applications to use
zope.configuration (ZCML). Most of these dependencies are actually
dependencies of ZCML (zope.configuration), rather than the ZCA
"proper" (zope.component).
In any case, on to the actual plugin-ization. To no one's surprise, the resulting plugin-ized version of the application is much more complex.
To actually plugin-ize the app, I made each operator into a named utility using the ZCA, configured via ZCML. The name of the utility is the operator itself. Each utility is registered via ZCML, ala:
<utility
provides=".interfaces.IOperator"
component=".operators.operator_add_token"
name="+"
/>
One utility is registered for each operator required. Accordingly, the application's tokenize() function now looks up each operator via a utility lookup:
def tokenize(program):
for number, operator in re.findall("\s*(?:(\d+)|(\*\*|.))", program):
if number:
yield literal_token(int(number))
elif operator:
utility = queryUtility(IOperator, name=operator)
if utility is None:
raise SyntaxError("unknown operator: %r" % operator)
yield utility()
else:
raise SyntaxError("unknown operator: %r" % operator)
yield end_token()
When an operator is encountered, queryUtility is run; it will try to
find a named utility (or it won't, and will raise a syntax error).
The utilities themselves are classes. For example, the
operator_add_token utility is defined as:
class operator_add_token(object):
""" plugin """
lbp = 10
def nud(self, context):
return context.expression(100)
def led(self, context, left):
return left + context.expression(10)
Note that I changed the application to use a "context" object rather
than module-scope globals to find the token and expression
callable, so this definition isn't exactly like the one defined by the
original application, but it still does the same thing.
In any case, all of the operators are defined in the same file
(calc.operators). This is just for convenience; they could be
defined all over hell and gone if you liked (you'd just change the
ZCML to refer to a utility component at a different dotted name). And of
course if you included more ZCML (which can cross files too), you'd
could add another operator or override the implementation of an
existing operator. I don't have very much imagination, so I did
neither. You get the point, hopefully.
I suppose this is about the simplest possible example of using the
Zope Component Architecture to create a pluggable application. You
can also define your own ZCML directives (e.g. I could have made the
ZCML read something like <registerOperator name="+"
implementation=".operators.operator_add">. You also don't really
need to use ZCML, it's just a shell around the actual component
architecture that makes clear the difference between code and
configuration.
The result of my toying around exists at http://plope.com/static/misc/calc-0.2.tar.gz . Run "setup.py install" (in a virtualenv, to prevent polluting your system Python with the above libraries) to install it. To run it subsequently, run "bin/calctest" (a setuptools console script).
grokcore.component-1.5.1-py2.4.egg
martian-0.11-py2.4.egg
zope.component-3.5.1-py2.4.egg
zope.configuration-3.4.1-py2.4.egg
zope.deferredimport-3.4.0-py2.4.egg
zope.deprecation-3.4.0-py2.4.egg
zope.event-3.4.0-py2.4.egg
zope.i18nmessageid-3.4.3-py2.4-linux-i686.egg
zope.interface-3.5.0-py2.4-linux-i686.egg
zope.proxy-3.4.2-py2.4-linux-i686.egg
zope.schema-3.5.0a2-py2.4.egg
zope.testing-3.7.1-py2.4.egg
http://regebro.wordpress.com/2008/12/19/the-plugin-architecture-bashout-grok/