diff -r 000000000000 -r d153941bb745 src/source/zca.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/source/zca.rst Sun Jan 14 11:48:51 2018 +0100 @@ -0,0 +1,263 @@ +.. _zca: + +Managing ZCA with PyAMS +======================= + +The **Zope Component Architecture** (aka 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. + +You will find several useful resources about ZCA concepts on the internet. + + +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**. + + +Defining site root +------------------ + +One of PyAMS pre-requisites is to use the ZODB, at least to store the site root application, it's configuration and a +set of local utilities. :ref:`site` describes application startup and **local site manager** initialization process. + +This site can be used to store **local utilities** whose configuration, which is easily available to site +administrators through management interface, is stored in the ZODB. + + +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: + +.. code-block:: python + + 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. + + +Registering local utilities +--------------------------- + +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: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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. + + +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: + +.. code-block:: python + + 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. + + +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: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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 interfaces and/or as classes. + + +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: + +.. code-block:: python + + 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)