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 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 are objects that are associated with interfaces.
- Utilities are just components that provide an interface and that are looked up by an interface and a name
- 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.
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¶
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.
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.
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)