[Zope3-checkins] CVS: Zope3/src/zope/app/services - utility.txt:1.1
Jim Fulton
jim@zope.com
Sun, 22 Jun 2003 09:28:18 -0400
Update of /cvs-repository/Zope3/src/zope/app/services
In directory cvs.zope.org:/tmp/cvs-serv17116
Added Files:
utility.txt
Log Message:
Reorganized in preparation for adding separate tutorials for different
flavors of services.
Split utility and error reporting examples out.
This file still needs a bit more work.
=== Added File Zope3/src/zope/app/services/utility.txt ===
==================================================
Creating Local Services: The Local Utility Service
==================================================
:Author: Jim Fulton
:Version: $Revision: 1.1 $
.. contents::
This document describes how to implement a common type of local
service. We'll walk through an example step by step. We'll implement
a local utility service. A utility service must implement the
interface ``zope.component.interfaces.IUtilityService``.
Step 1. Create a minimal service
--------------------------------
Create a minimal service that delagates everything to the
service above it, in the file ``utility.py``::
from persistence import Persistent
from zope.component.exceptions import ComponentLookupError
from zope.context import ContextMethod
from zope.app.component.nextservice import getNextService
from zope.component.interfaces import IUtilityService
from zope.app.interfaces.services.interfaces import ISimpleService
from zope.interface import implements
class LocalUtilityService(Persistent):
implements(IUtilityService, ISimpleService)
def getUtility(self, interface, name=''):
utility = self.queryUtility(interface, name=name)
if utility is None:
raise ComponentLookupError("utility", interface, name)
return utility
getUtility = ContextMethod(getUtility)
def queryUtility(self, interface, default=None, name=''):
next = getNextService(self, "Utilities")
return next.queryUtility(interface, default, name)
queryUtility = ContextMethod(queryUtility)
The local service subclasses ``Persistent`` to provide support for
transparent persistent in the ZODB.
The local service uses the `ContextMethod`` function to convert it's
methods to be context methods. Context methods are called with "self"
arguments that are context wrapped. This is needed if the methods or
properties are going to call APIs that need acquisition context.
The ``getUtility`` method simply delegates to ``queryUtility``. The
``queryUtility`` method delegates to the next utility service using
``getNextService``. (Both methods are specified by the
``IUtilityService`` interface.)
The function ``getNextService`` looks up the next service above the
current service. It takes a location and a service name. We use it
to get the interface service defined above our service, which may be
the global service, and delegate to it.
In addition to implementing ``IUtilityService``, the local service
implements ``ISimpleService``. A Local service must implement
``zope.app.interfaces.services.interfaces.ILocalService`` and a local
service must be annotatable. ``ISimpleService`` simply extends
``ILocalService`` and ``IAttributeAnnotatable``.
I created the service in the ``utility`` module in this package (the
file ``utility.py``). This package is already pretty large. To avoid
a really large zcml file, I've started giving each service its own
zcml file. So I also created an ``utility.zcml`` file::
<zopeConfigure xmlns="http://namespaces.zope.org/zope">
<content class=".utility.LocalUtilityService">
<factory
id="zope.app.services.UtilityService"
permission="zope.ManageServices"
/>
</content>
</zopeConfigure>
and I added an include to the package configuration file::
<!-- Utility Service --> <include file="utility.zcml" />
To make it possible to add the utility service, I need to add an entry to
the ``add_service`` browser menu. The ``add_component`` menu is the menu
used by site folders for adding objects. To do this, I need to add a
browser menu configuration. Eventually, the local interface will
have a number of views, so I create a package, ``utility``, for
it in ``zope/app/browser/services``. [1]_ In that
package, I put a configuration that defines the needed browser menu
item::
<zopeConfigure xmlns="http://namespaces.zope.org/browser">
<menuItem
for="zope.app.interfaces.container.IAdding"
menu="add_service"
action="zope.app.services.UtilityService"
title="Utility Service"
permission="zope.ManageServices"
/>
</zopeConfigure>
and I added an include to the configuration file in
zope.app.browser.services::
<!-- Utility Service --> <include package=".utility" />
With this in place, I can add a local service that does nothing but
delegate to a service above it. (To actually create a utility service
instance, I have to go to the service manager and use its ``Add
service`` action. The service manager is reached from the root folder
by using the ``Manage local services`` action.)
Step 2. Providing functionality
-------------------------------
Now it's time to add some functionality. A utility service keeps
track of utility components by name and interface. It allows
components to be registered and then looked up later.
We'll start by updating the utility service to support registrations.
The updated local utility service implementation can be found in
zope/app/services/utility.py.
First, we'll pick a data structure. We'll use a persistent dictionary
mapping utility names to implementor registries. An implementor
registry implements a mapping from interfaces to objects; it's not
quite the same as a mapping because it understands subclassing
relationships between the interfaces used as keys. In this case, the
implementor registries themselves map interfaces to
RegistrationStacks::
{ utility_name -> { interface -> stack } }
We also need to implement
zope.app.interfaces.services.registration.IRegistry. This defines two
methods, ``queryRegistrationsFor`` and ``createRegistrationsFor``.
A ``queryRegistrationsFor`` method is added to implement
``IRegistry``. It takes a registration object and returns the
corresponding registration registry. The registration object is
used to provide an abstract way to represent registration parameters.
Typically, the registration parameters are extracted and a more
concrete method is called. In the local utility service, we extract
the utility name and interface and call ``queryRegistrations`` with
the name and interface.
Similarly, we add a ``createRegistrationsFor`` method that takes a
registration object holding registration parameters and creates a
registration registry for the parameters (if none already exists).
If we don't have a implementor registry for a utility name, we create
one and add it. When we create the implementor registry, we pass a
``PersistentDict`` for it to use to store registration data. This
assures that updates are made persistently. If there isn't implementor
data for the given interface, we create a registration registry and
register it for the interface.
Finally, we modify ``queryUtility`` to use registered utility
registrations. We try to get a registration registery by calling
``queryRegistrations``. If we get one, we call its ``active``
method to get the active registration, if any. Finally, we call
``getComponent`` on the active registration to get the actual
component. We leave it up to the registration object to take care of
actually finding and returning the component.
We need to provide utility registration objects. The
utility registration objects need to manage several bits of
information:
- name
- interface
- permission
- The location of the actual component.
The registration objects provide access to the component through the
getComponent method.
To create the registration class, we'll start by defining a
registration schema in
``zope/app/interfaces/services/utility.py``. The schema should extend
``zope.app.interfaces.services.registration.IRegistration``.
There's a more specific interface,
``zope.app.interfaces.services.registration.IComponentRegistration``
that is much closer to what we need. (XXX Add footnote explaining why
we can't use INamedComponentRegistration in this example.)
We extend this interface as IUtilityRegistration, which adds a name
field (which is required but may be empty -- note the subtle
difference, because the empty string is still used as part of the
lookup key) and an interface field. We also override the
componentPath field to make it read-only (this is for the UI
definition).
A ``UtilityRegistration`` class is added to the ``utility`` module in
``zope/app/services`` that implements the registration interface. We
can subclass ComponentRegistration, which does much of the work. Note
that registration component includes two methods, defined in
``IRegistration``, giving summary information used by several
predefined views. See the interface for a description of these methods.
We need to provide user interfaces for:
- The utility service,
- The utility registrations, and for
- Registered utilities
Utility service user interface
==============================
The utility service needs a view for showing what utilities have been
registered.
MORE DOCUMENTATION TO FOLLOW
Utility registration user interface
===================================
We need two views for registration objects:
- an add view, and
- an edit view
The registration add view will be created using a schema-driven add
form::
<addform
label="New Utility Registration"
for="zope.app.interfaces.services.utility.ILocalUtility"
name="addRegistration.html"
schema="zope.app.interfaces.services.utility.IUtilityRegistration"
class=".AddRegistration"
permission="zope.ManageServices"
content_factory="zope.app.services.utility.UtilityRegistration"
arguments="name interface componentPath"
set_after_add="status"
fields="name interface componentPath permission status"
/>
The add form is named ``addRegistration.html``. This is a standard
name that a number of other views expect to be defined. The form is
registered for ``ILocalUtility``. ``ILocalUtility`` is a marker
interface thgat extends ``IRegisterable``. We'll require that all
local utilities implement this view. Utility components should also
implement IAttributeAnnotatable, unless they want to provide a
different way to store annotations.
Notice that there's no template! The <addform> directive creates the
form for us using a generic template, zope/app/browser/form/add.pt,
and information about the specific fields to be displayed extracted
from the schema. We do specify a class name: the AddRegistration
class. This class needs some explanation.
The <addform> directive uses the AddRegistration class as a mix-in
class. It may override various methods to customize the add form; the
set of methods that can be customized is described by the
``zope.app.interfaces.browser.form.IAddFormCustomization`` interface.
In this particular case, we must override ``add`` and ``nextURL``
because their default implementations only work when the add form is a
view on an `IAdding` view. That is the normal way to use add forms, but
here we don't do that; this particular add form is a view on a local
utility component. Our ``AddRegistration`` class subclasses
``zope.app.browser.services.registration.AddComponentRegistration``,
which provides the implementations of ``add`` and ``nextURL`` that we
need. The ``add`` method defined in ``AddComponentRegistration`` finds
the congiguration manager in the current folder and adds the new
registration object to it.
The AddRegistration class defines a class attribute::
interface_widget = CustomWidget(UtilityInterfaceWidget)
This tells the forms machinery to use a a custom widget for the
interface field. The custom widget we use is a specialized interface
widget that allows the user to select from the non-trivial interfaces
implemented by the component being configured.
The edit view looks similar to the add view, but its definition
is simpler, because it isn't deviating quite as much from a standard
edit view::
<editform
name="index.html"
menu="zmi_views" title="Edit"
schema="zope.app.interfaces.services.utility.IUtilityRegistration"
label="Utility Registration"
permission="zope.ManageServices"
fields="name interface componentPath permission status"
/>
This is a view on IUtilityRegistration, which is typical for an edit
view. The good news is that it has no template *or* class! The
<editform> directive lets us specifiy all the customization we need:
- ``name=``: The view name. This is the last component of the URL for
the view.
- ``menu=``, ``title=``: Menu information: "zmi_views" means that this
view is one of the "tabs" for this type of object, the title
argument gives the text in the tab.
- ``schema=``: The interface used as the schema. The field
definitions in this interface define which attributes are displayed
in the form, and how.
- ``label=``: The label, used as the title text in the view.
- ``permission=``: A permission needed to access the view.
- ``fields=``: A list of fields to be displayed in the form. This is
used here to force the order in which the fields are displayed. It
can also be used to display only a subset of the fields present in
the schema.
And that's all there is to the edit view. Some observable differences
between the edit view and the add view:
- The add view lets you specify the name or the interface; the edit
view displays these fields read-only.
- When you submit the add view, you are redirected to the
registration manager; the edit view takes you back to itself.
User interface for registered utilities
=======================================
In general, for registerable components, we want to do
the registration through the components themselves. That is, the site
admin should be able to walk up to a component and add or change a
registration for it. The most common case is actually that a site
manager creates a component and configures it right away. (This is
the same common case that is used for creating and configuring
services.) There's a standard view registered for all IRegisterable
components that provides the ability to view and change the
registrations for a component.
IRegisterable is a marker interface that extends IAnnotatable.
Annotations are used to record the registrations for a component.
---------------------------------------------------------------
.. [1] Of course, I initially forgot to include a nearly empty
``__init__.py`` file and had to add one later.