Zope Component Architecture with PyAMS

PyAMS packages are developed based on the Zope Component Architecture (aka ZCA). ZCA is used by the Pyramid framework “under the hood” to handle interfaces, adapters and utilities. You don’t have to use it in your own applications. But you can.

The ZCA is mainly adding elements like interfaces, adapters and utilities to the Python language. It allows you to write a framework or an application by using components which can be extended easily.

Interfaces
Interfaces are objects that specify (document) the external behavior of objects that “provide” them. An interface specifies behavior through, a documentation in a doc string, attribute definitions and conditions of attribute values.
Components
Components are objects that are associated with interfaces.
Utilities
Utilities are just components that provide an interface and that are looked up by an interface and a name
Adapters
Adapters are components that are computed from other components to adapt them to some interface. Because they are computed from other objects, they are provided as factories, usually classes.

You will find several useful resources about ZCA concepts on the internet.

Utilities

Local utilities

In ZCA, a utility is a registered component which provides an interface. This interface is the contract which defines features (list of attributes and methods) provided by the component which implements it.

When a Pyramid application starts, a global registry is created to register a whole set of utilities and adapters; this registration can be done via ZCML directives or via native Python code. In addition, PyAMS allows you to define local utilities, which are stored and registered in the ZODB via a site manager.

Registering local utilities

Tip

Site Manager guide can be used to store local utilities whose configuration, which is easily available to site administrators through management interface, is stored in the ZODB.

A local utility is a persistent object, registered in a local site manager, and providing a specific interface (if a component provides several interfaces, it can be registered several times).

Some components can be required by a given package, and created automatically via the pyams_upgrade command line script; this process relies on the ISiteGenerations interface, for example for the timezone utility, a component provided by PyAMS_utils package to handle server timezone and display times correctly:

from pyams_utils.interfaces.site import ISiteGenerations
from pyams_utils.interfaces.timezone import IServerTimezone

from persistent import Persistent
from pyams_utils.registry import utility_config
from pyams_utils.site import check_required_utilities
from pyramid.events import subscriber
from zope.container.contained import Contained
from zope.interface import implementer
from zope.schema.fieldproperty import FieldProperty

@implementer(IServerTimezone)
class ServerTimezoneUtility(Persistent, Contained):

    timezone = FieldProperty(IServerTimezone['timezone'])

REQUIRED_UTILITIES = ((IServerTimezone, '', ServerTimezoneUtility, 'Server timezone'),)

@subscriber(INewLocalSite)
def handle_new_local_site(event):
    """Create a new ServerTimezoneUtility when a site is created"""
    site = event.manager.__parent__
    check_required_utilities(site, REQUIRED_UTILITIES)

@utility_config(name='PyAMS timezone', provides=ISiteGenerations)
class TimezoneGenerationsChecker(object):
    """Timezone generations checker"""

    generation = 1

    def evolve(self, site, current=None):
        """Check for required utilities"""
        check_required_utilities(site, REQUIRED_UTILITIES)

Some utilities can also be created manually by an administrator through the management interface, and registered automatically after their creation. For example, this is how a ZEO connection utility (which is managing settings to define a ZEO connection) is registered:

from pyams_utils.interfaces.site import IOptionalUtility
from pyams_utils.interfaces.zeo import IZEOConnection
from zope.annotation.interfaces import IAttributeAnnotatable
from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent

from persistent import Persistent
from pyramid.events import subscriber
from zope.container.contained import Contained

@implementer(IZEOConnection)
class ZEOConnection(object):
    """ZEO connection object. See source code to get full implementation..."""

@implementer(IOptionalUtility, IAttributeAnnotatable)
class ZEOConnectionUtility(ZEOConnection, Persistent, Contained):
    """Persistent ZEO connection utility"""

@subscriber(IObjectAddedEvent, context_selector=IZEOConnection)
def handle_added_connection(event):
    """Register new ZEO connection when added"""
    manager = event.newParent
    manager.registerUtility(event.object, IZEOConnection, name=event.object.name)

@subscriber(IObjectRemovedEvent, context_selector=IZEOConnection)
def handle_removed_connection(event):
    """Un-register ZEO connection when deleted"""
    manager = event.oldParent
    manager.unregisterUtility(event.object, IZEOConnection, name=event.object.name)

context_selector is a custom subscriber predicate, so that subscriber event is activated only if object concerned by an event is providing given interface.

Registering global utilities

Global utilities are components providing an interface which are registered in the global registry. PyAMS_utils package provides custom annotations to register global utilities without using ZCML. For example, a skin is nothing more than a simple utility providing the ISkin interface:

from pyams_default_theme.layer import IPyAMSDefaultLayer
from pyams_skin.interfaces import ISkin
from pyams_utils.registry import utility_config

@utility_config(name='PyAMS default skin', provides=ISkin)
class PyAMSDefaultSkin(object):
    """PyAMS default skin"""

    label = _("PyAMS default skin")
    layer = IPyAMSDefaultLayer

This annotation registers a utility, named PyAMS default skin, providing the ISkin interface. It’s the developer responsibility to provide all attributes and methods required by the provided interface.

Looking for utilities

ZCA provides the getUtility and queryUtility functions to look for a utility. But these methods only applies to global registry.

PyAMS package provides equivalent functions, which are looking for components into local registry before looking into the global one. For example:

from pyams_security.interfaces import ISecurityManager
from pyams_utils.registry import query_utility

manager = query_utility(ISecurityManager)
if manager is not None:
    print("Manager is there!")

All ZCA utility functions have been ported to use local registry: registered_utilities, query_utility, get_utility, get_utilities_for, get_all_utilities_registered_for functions all follow the equivalent ZCA functions API, but are looking for utilities in the local registry before looking in the global registry.

Adapters

Registering adapters

An adapter is also a kind of utility. But instead of just providing an interface, it adapts an input object, providing a given interface, to provide another interface. An adapter can also be named, so that you can choose which adapter to use at a given time.

PyAMS_utils provide another annotation, to help registering adapters without using ZCML files. An adapter can be a function which directly returns an object providing the requested interface, or an object which provides the interface.

The first example is an adapter which adapts any persistent object to get it’s associated transaction manager:

from persistent.interfaces import IPersistent
from transaction.interfaces import ITransactionManager
from ZODB.interfaces import IConnection

from pyams_utils.adapter import adapter_config

@adapter_config(context=IPersistent, provides=ITransactionManager)
def get_transaction_manager(obj):
    conn = IConnection(obj)
    try:
        return conn.transaction_manager
    except AttributeError:
        return conn._txn_mgr

This is another adapter which adapts any contained object to the IPathElements interface; this interface can be used to build index that you can use to find objects based on a parent object:

from pyams_utils.interfaces.traversing import IPathElements
from zope.intid.interfaces import IIntIds
from zope.location.interfaces import IContained

from pyams_utils.adapter import ContextAdapter
from pyams_utils.registry import query_utility
from pyramid.location import lineage

@adapter_config(context=IContained, provides=IPathElements)
class PathElementsAdapter(ContextAdapter):
    """Contained object path elements adapter"""

    @property
    def parents(self):
        intids = query_utility(IIntIds)
        if intids is None:
            return []
        return [intids.register(parent) for parent in lineage(self.context)]

An adapter can also be a multi-adapter, when several input objects are requested to provide a given interface. For example, many adapters require a context and a request, eventually a view, to provide another feature. This is how, for example, we define a custom name column in a security manager table displaying a list of plug-ins:

from pyams_zmi.layer import IAdminLayer
from z3c.table.interfaces import IColumn

from pyams_skin.table import I18nColumn
from z3c.table.column import GetAttrColumn

@adapter_config(name='name', context=(Interface, IAdminLayer, SecurityManagerPluginsTable), provides=IColumn)
class SecurityManagerPluginsNameColumn(I18nColumn, GetAttrColumn):
    """Security manager plugins name column"""

    _header = _("Name")
    attrName = 'title'
    weight = 10

As you can see, adapted objects can be given as functions or as classes.

Vocabularies

Registering vocabularies

A vocabulary is a custom factory which can be used as source for several field types, like choices or lists. Vocabularies have to be registered in a custom registry, so PyAMS_utils provide another annotation to register them. This example is based on the Timezone component which allows you to select a timezone between a list of references:

import pytz
from pyams_utils.vocabulary import vocabulary_config
from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary

@vocabulary_config(name='PyAMS timezones')
class TimezonesVocabulary(SimpleVocabulary):
    """Timezones vocabulary"""

    def __init__(self, *args, **kw):
        terms = [SimpleTerm(t, t, t) for t in pytz.all_timezones]
        super(TimezonesVocabulary, self).__init__(terms)