[Zope3-checkins] CVS: Zope3/doc/zcml - meta.stx:1.1
R. David Murray
bitz@bitdance.com
Wed, 18 Sep 2002 18:53:09 -0400
Update of /cvs-repository/Zope3/doc/zcml
In directory cvs.zope.org:/tmp/cvs-serv11259
Added Files:
meta.stx
Log Message:
First draft of a user guide (not reference) to the meta-configuration
system. Note that the example code, while inspired by Stephan's
StartUp code, is *not* Stephan's StartUp code <grin>. Nor did I
actually test the example code (yet).
Comments and patches welcome.
=== Added File Zope3/doc/zcml/meta.stx ===
The Meta Configuration System
Meta configuration is the configuration of the configuration
system. In the case of zcml, this means using the "bootstrap"
zcml configuration directives to define the configuration directives
that will actually be used to configure the system.
The Meta Configuration Directives
The "bootstrap" directives are 'directives', 'directive', and
'subdirective'. We'll use a shortened version of the
meta-configuration for the Zope StartUp system to explain the
concepts::
<ZopeConfigure xmlns='http://namespaces.zope.org/zope">
<directives namespace="http://namespaces.zope.org/startup">
<directive
name="registerRequestFactory"
attributes="name publication request"
handler="Zope.StartUp.metaConfigure.registerRequestFactory" />
<directive
name="defineSite"
attributes="name threads"
handler="Zope.Startup.metaConfigure.defineSite">
<subdirective name="useFileStorage" attributes="file" />
<subdirective name="useMappingStorage" />
</directive>
</directives>
</ZopeConfigure>
The ZopeConfigure is the standard boilerplate. Inside this we
have a 'directives' directive. This directive basically gives
us a place to declare the namespace the contained directives
will be in so we don't have to repeat it on each directive
declaration. The namespace attribute gives the XML namespace
for the directives. You do not have to use 'directives',
'directive' is also valid as a top level directive, although
in that case the 'namespace' attribute is required rather
than optional.
The first example directive directive defines a directive that
has no subdirectives. The 'name' attribute gives the name of
the directive. So in this case we are defining the
'registerRequestFactory' directive. Combining the namespace
with this name, this means we are defining a directive that
would look something like this when used::
<ZopeConfigure
xmlns='http://namespaces.zope.org/zope"
startup='http://namespaces.zope.org/startup"
>
<startup:registerRequestFactory ....>
</ZopeConfigure>
The "attributes" attribute of the directive directive allows
us to fill in the "...." in the example above. It lists the
names of the attributes that will be valid for this directive.
So this meta-configuration is saying that the registerRequestFactory
directive can have attributes named 'name', 'publication', or
'request'.
The directive we are defining can thus look something like this
when used::
<ZopeConfigure
xmlns='http://namespaces.zope.org/zope"
startup='http://namespaces.zope.org/startup"
>
<startup:registerRequestFactory
name="VFSRequestFactory"
publication="Zope.App.Publication.VFS.Publication.VFSPublication"
request="Zope.Publisher.VFS.VFSRequest." />
</ZopeConfigure>
Which attributes are optional and which are required is controlled
by the python code that implements the directive.
The 'handler' attribute is what defines which python code will
handle the directive. It must be a resolvable name following
the standard zcml rules.
The second directive example shows how to define a directive
that can take subdirectives. Subdirectives are only meaningful
when they appear inside their enclosing directive, and in fact
each directive is effectively its own namespace from this point
of view.
There is no need to specify a handler method explicitly for a
subdirective. By default, the configuration system will look
for a method with the same name as the subdirective on an object
specified by the handler for the enclosing directive. If this
is not correct, and the handler method is named something else,
you can use the 'handler_method' attribute to specify the correct
name to be looked up.
Note that subdirectives may have subdirectives.
So, given the zcml above, we have defined a directive and
subdirective that would look something like this when used::
<ZopeConfigure
xmlns='http://namespaces.zope.org/zope"
startup='http://namespaces.zope.org/startup"
>
<startup:defineSite name="Zope 3 Default" threads="4">
<startup:useFileStorage file="Data.fs">
</startup:defineSite>
</ZopeConfigure>
How Configuration Directives Become Actions
When the configuration system processes configuration directives,
it calls the handlers for each directive or subdirective it
encounters. But the handler method is *not* responsible for
taking whatever action it is that the directive is supposed to
accomplish. Instead, the handler is responsible for the
generation of a list of "actions" accompanied by "discriminators".
The configuration system uses the discriminators to resolve
conflicts between directives. (Recall that in case of conflict,
an action returned by an included file will be overridden, while
conflicts generated within the same file will be treated as
errors.)
An "action" is a python tuple with the following elements
(IEmptyDirective is the canonical source for this definition)::
- the discriminator
- a callable object
- an argument tuple
- an optional keyword argument dictionary
Once conflict resolution has been done, the configuration system
processes the actions one by one by calling each callable and
passing it the provided argument tuple and keyword dictionary.
Thus, to implement directives, we implement handlers that manage
the generation of action tuples encoding the calls to the methods
that will actually perform the configuration actions.
To aid python code in generating correctly formed Actions, there
is an Action function defined in Zope.Configuration.Action. It
takes four keyword arguments, 'discriminator', 'callable', 'args',
and 'kw', which have the obvious meanings, and returns a properly
arranged tuple.
Implementing Directives with No Subdirectives
The simplist type of directive (or subdirective) to implement
is one that has no subdirectives. And the only difference
between to two is where the handler method is actually located.
In both cases, the handler must conform to the IEmptyDirective
interface. That interface mandates that when called, the handler
will return a list of actions. So, our registerRequestFactory
directive above might have implementation code that looks
something like this::
from Zope.Configuration.Action import Action
from Zope.StartUp.RequestFactory import RequestFactory
def registerRequestFactory(_context, name, publication, request):
publication = _context.resolve(publication)
request = _context.resolve(request)
return [
Action(
discriminator = ('startup:registerRequestFactory', name),
callable = RequestFactoryRegistry.registerRequestFactory
args = (name, publication, request),
)
]
registerRequestFactory.__implements__ = IEmptyDirective
The first argument passed to the handler is an "execution
context". The most important method it provides is 'resolve',
which will take a dotted string and resolve it into a python
object using the standard zcml rules. This method uses it to
turn the values of the directive's publication and request
attributes into python objects. The callable for the action
is obtained from one of the other modules in the package. It's
the code that will do the actual setup work this directive is
intended to accomplish. The values of the 'name', 'publication',
and 'request' attributes, in resolved form, are included in the
Action to be passed to it as arguments.
Note that we include the name of the directive, including the
conventional short form of its namespace name, as part of a
tuple to be used as the discriminator. This gives us reasonable
assurance that this discriminator will match only other instances
of registerRequestFactory directives that the configuration
user might be specifying as overrides. The discriminator is
arbitrary, but the programmer needs to take care that it is
exactly as unique as it needs to be within the set of all actions
the configuration system may need to deal with.
Also note that by using positional arguments only we have made
all the attributes required. If any are omitted from the
directive, python will complain about missing arguments. Making
an argument a keyword argument makes the correspondingly named
attribute optional.
Implementing Directives that have Subdirectives
The handler for a directive that has subdirectives must conform
to the INonEmptyDirective Interface. This means that when
called it must return an object that implements the
ISubdirectiveHandler Interface.
When a subdirective is defined, a handler method for the
subdirective is either specified or defaults to the name of the
subdirective. This handler will be looked up on the
ISubdirectiveHandler that the INonEmptyDirective returns.
When a directive with subdirectives is processed, first the
INonEmptyDirective is called to get the ISubdirectiveHandler.
Then each subdirective is processed by calling the appropriate
method on the ISubdirectiveHandler. Finally, the ISubdirectiveHandler
itself is called, to give it an opportunity to specify
directive-level actions (it does not have to, it can return an
empty action list).
The easiest way to implement an INonEmptyDirective is to create
a class with the subdirective methods and a '__call__' method
on it. 'Calling' the class (that is, instantiating an instance)
then results in an object that implements ISubdirectiveHandler.
For example, 'Zope.Startup.metaConfigure.defineSite' might be
defined as follows::
from Zope.StartUp.SiteDefinition import SiteDefinition
class defineSite:
__class_implements__ = INonEmptyDirective
__implements__ = ISubdirectiveHandler
def __init__(self, _context, name="default" threads=4):
SiteDefinition.registerSite(name)
self._name = name
self._threads = int(threads)
def useFileStorage(self, _context, file=DEFAULT_STORAGE_FILE):
return [
Action(
discriminator=('startup:defineSite','storage',name),
callable=SiteDefinition.useFileStorage,
args=(self._name,file,)
)
]
def useMappingStorage(self, _context):
return [
Action(
discriminator=('startup:defineSite','storage',name),
callable=SiteDefinition.useMappingStorage,
args=(self._name,)
)
]
def __call__(self):
return [
Action(
discriminator=('startup:defineSite','threads',name),
callable=SiteDefinition.setThreads,
args=self._name,self._threads)
)
]
The two implements equates document the fact that the class
implements INonEmptyDirective, while the objects returned by
it implement ISubdirectiveHandler.
In the init, we save the attribute information we've been passed
so we can use it both in processing the subdirectives and in
the actions returned by the final call. We also call the object
that manages the actual configuration to tell it to initialize
a configuration for the site that has been named. (Note: an
alternative implementation would be to make the ISubdireciveHandler
a singleton, in which case it could store the configuration
information directly on itself).
The two subdirective methods use the same discriminator, and
the name of the site being defined is included in the discriminator.
This means that a given site can only use one or the other
storage. If both subdirectives are specified for the same site
in the same configuration file, we'll get a configuration error.
On the other hand, if one storage is specified in a given
configuration file, but the configuration file that includes
it (or some file above it) specifies a different storage, then
the conflict resolution will allow the upper level file to
override the lower.
The action method returned by the storage directives will
actually set the configuration to the specified storage type.
Only the storage setting action that has made it through conflict
resolution will get executed.
The '__call__' method of the ISubdirectiveHandler returns an
action that will tell the configuration object to set the thread
count for the site being defined. Again, if a site definition
in an included file specifies a thread count, it will get
overridden by a site definition in an upper level file because
of the discriminator conflict resolution.