src/source/dev_guide/zca.rst
branchdoc-dc
changeset 111 097b0c025eec
parent 109 31b3d00edb8a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/source/dev_guide/zca.rst	Tue Dec 11 17:00:42 2018 +0100
@@ -0,0 +1,292 @@
+.. _zca:
+
+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.
+
+.. seealso::
+    Zope Documentations:
+        - `Components and Interfaces <http://zope.readthedocs.io/en/latest/zdgbook/ComponentsAndInterfaces.html>`_
+        - `Zope component <http://zopecomponent.readthedocs.io/en/latest/narr.html>`_
+        - `Zope interface <https://docs.zope.org/zope.interface/README.html>`_
+
+
+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::
+
+    :ref:`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.
+
+
+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.
+
+
+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.
+
+
+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.
+
+
+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:
+
+.. 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 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:
+
+.. 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)