docs/source/zca.rst
changeset 72 9049384a2bd4
equal deleted inserted replaced
71:01d01045a2b7 72:9049384a2bd4
       
     1 .. _zca:
       
     2 
       
     3 Managing ZCA with PyAMS
       
     4 =======================
       
     5 
       
     6 The **Zope Component Architecture** (aka ZCA) is used by the Pyramid framework "under the hood" to handle interfaces,
       
     7 adapters and utilities. You don't **have to** use it in your own applications. But you can.
       
     8 
       
     9 The ZCA is mainly adding elements like **interfaces**, **adapters** and **utilities** to the Python language. It
       
    10 allows you to write a framework or an application by using **components** which can be extended easily.
       
    11 
       
    12 You will find several useful resources about ZCA concepts on the internet.
       
    13 
       
    14 
       
    15 Local utilities
       
    16 ---------------
       
    17 
       
    18 In ZCA, a **utility** is a **registered** component which provides an **interface**. This interface is the
       
    19 **contract** which defines features provided by the component which implements it.
       
    20 
       
    21 When a Pyramid application starts, a **global registry** is created to register a whole set of utilities and
       
    22 adapters; this registration can be done via ZCML directives or via native Python code.
       
    23 In addition, PyAMS allows you to define **local utilities**, which are stored and registered in the ZODB via a **site
       
    24 manager**.
       
    25 
       
    26 
       
    27 Defining site root
       
    28 ------------------
       
    29 
       
    30 One of PyAMS pre-requisites is to use the ZODB, at least to store the site root application, it's configuration and a
       
    31 set of local utilities. :ref:`site` describes application startup and **local site manager**
       
    32 initialization process.
       
    33 
       
    34 This site can be used to store **local utilities** whose configuration, which is easily available to site
       
    35 administrators through management interface, is stored in the ZODB.
       
    36 
       
    37 
       
    38 Registering global utilities
       
    39 ----------------------------
       
    40 
       
    41 **Global utilities** are components providing an interface which are registered in the global registry.
       
    42 PyAMS_utils package provides custom annotations to register global utilities without using ZCML. For example, a skin
       
    43 is nothing more than a simple utility providing the *ISkin* interface:
       
    44 
       
    45 .. code-block:: python
       
    46 
       
    47     from pyams_default_theme.layer import IPyAMSDefaultLayer
       
    48     from pyams_skin.interfaces import ISkin
       
    49     from pyams_utils.registry import utility_config
       
    50 
       
    51     @utility_config(name='PyAMS default skin', provides=ISkin)
       
    52     class PyAMSDefaultSkin(object):
       
    53         """PyAMS default skin"""
       
    54 
       
    55         label = _("PyAMS default skin")
       
    56         layer = IPyAMSDefaultLayer
       
    57 
       
    58 This annotation registers a utility, named *PyAMS default skin*, providing the *ISkin* interface. It's the developer
       
    59 responsibility to provide all attributes and methods required by the provided interface.
       
    60 
       
    61 
       
    62 Registering local utilities
       
    63 ---------------------------
       
    64 
       
    65 A local utility is a persistent object, registered in a *local site manager*, and providing a specific interface (if
       
    66 a component provides several interfaces, it can be registered several times).
       
    67 
       
    68 Some components can be required by a given package, and created automatically via the *pyams_upgrade* command line
       
    69 script; this process relies on the *ISiteGenerations* interface, for example for the timezone utility, a component
       
    70 provided by PyAMS_utils package to handle server timezone and display times correctly:
       
    71 
       
    72 .. code-block:: python
       
    73 
       
    74     from pyams_utils.interfaces.site import ISiteGenerations
       
    75     from pyams_utils.interfaces.timezone import IServerTimezone
       
    76 
       
    77     from persistent import Persistent
       
    78     from pyams_utils.registry import utility_config
       
    79     from pyams_utils.site import check_required_utilities
       
    80     from pyramid.events import subscriber
       
    81     from zope.container.contained import Contained
       
    82     from zope.interface import implementer
       
    83     from zope.schema.fieldproperty import FieldProperty
       
    84 
       
    85     @implementer(IServerTimezone)
       
    86     class ServerTimezoneUtility(Persistent, Contained):
       
    87 
       
    88         timezone = FieldProperty(IServerTimezone['timezone'])
       
    89 
       
    90     REQUIRED_UTILITIES = ((IServerTimezone, '', ServerTimezoneUtility, 'Server timezone'),)
       
    91 
       
    92     @subscriber(INewLocalSite)
       
    93     def handle_new_local_site(event):
       
    94         """Create a new ServerTimezoneUtility when a site is created"""
       
    95         site = event.manager.__parent__
       
    96         check_required_utilities(site, REQUIRED_UTILITIES)
       
    97 
       
    98     @utility_config(name='PyAMS timezone', provides=ISiteGenerations)
       
    99     class TimezoneGenerationsChecker(object):
       
   100         """Timezone generations checker"""
       
   101 
       
   102         generation = 1
       
   103 
       
   104         def evolve(self, site, current=None):
       
   105             """Check for required utilities"""
       
   106             check_required_utilities(site, REQUIRED_UTILITIES)
       
   107 
       
   108 Some utilities can also be created manually by an administrator through the management interface, and registered
       
   109 automatically after their creation. For example, this is how a ZEO connection utility (which is managing settings to
       
   110 define a ZEO connection) is registered:
       
   111 
       
   112 .. code-block:: python
       
   113 
       
   114     from pyams_utils.interfaces.site import IOptionalUtility
       
   115     from pyams_utils.interfaces.zeo import IZEOConnection
       
   116     from zope.annotation.interfaces import IAttributeAnnotatable
       
   117     from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
       
   118 
       
   119     from persistent import Persistent
       
   120     from pyramid.events import subscriber
       
   121     from zope.container.contained import Contained
       
   122 
       
   123     @implementer(IZEOConnection)
       
   124     class ZEOConnection(object):
       
   125         """ZEO connection object. See source code to get full implementation..."""
       
   126 
       
   127     @implementer(IOptionalUtility, IAttributeAnnotatable)
       
   128     class ZEOConnectionUtility(ZEOConnection, Persistent, Contained):
       
   129         """Persistent ZEO connection utility"""
       
   130 
       
   131     @subscriber(IObjectAddedEvent, context_selector=IZEOConnection)
       
   132     def handle_added_connection(event):
       
   133         """Register new ZEO connection when added"""
       
   134         manager = event.newParent
       
   135         manager.registerUtility(event.object, IZEOConnection, name=event.object.name)
       
   136 
       
   137     @subscriber(IObjectRemovedEvent, context_selector=IZEOConnection)
       
   138     def handle_removed_connection(event):
       
   139         """Un-register ZEO connection when deleted"""
       
   140         manager = event.oldParent
       
   141         manager.unregisterUtility(event.object, IZEOConnection, name=event.object.name)
       
   142 
       
   143 *context_selector* is a custom subscriber predicate, so that subscriber event is activated only if object concerned
       
   144 by an event is providing given interface.
       
   145 
       
   146 
       
   147 Looking for utilities
       
   148 ---------------------
       
   149 
       
   150 ZCA provides the *getUtility* and *queryUtility* functions to look for a utility. But these methods only applies to
       
   151 global registry.
       
   152 
       
   153 PyAMS package provides equivalent functions, which are looking for components into local registry before looking into
       
   154 the global one. For example:
       
   155 
       
   156 .. code-block:: python
       
   157 
       
   158     from pyams_security.interfaces import ISecurityManager
       
   159     from pyams_utils.registry import query_utility
       
   160 
       
   161     manager = query_utility(ISecurityManager)
       
   162     if manager is not None:
       
   163         print("Manager is there!")
       
   164 
       
   165 All ZCA utility functions have been ported to use local registry: *registered_utilities*, *query_utility*,
       
   166 *get_utility*, *get_utilities_for*, *get_all_utilities_registered_for* functions all follow the equivalent ZCA
       
   167 functions API, but are looking for utilities in the local registry before looking in the global registry.
       
   168 
       
   169 
       
   170 Registering adapters
       
   171 --------------------
       
   172 
       
   173 An adapter is also a kind of utility. But instead of *just* providing an interface, it adapts an input object,
       
   174 providing a given interface, to provide another interface. An adapter can also be named, so that you can choose which
       
   175 adapter to use at a given time.
       
   176 
       
   177 PyAMS_utils provide another annotation, to help registering adapters without using ZCML files. An adapter can be a
       
   178 function which directly returns an object providing the requested interface, or an object which provides the interface.
       
   179 
       
   180 The first example is an adapter which adapts any persistent object to get it's associated transaction manager:
       
   181 
       
   182 .. code-block:: python
       
   183 
       
   184     from persistent.interfaces import IPersistent
       
   185     from transaction.interfaces import ITransactionManager
       
   186     from ZODB.interfaces import IConnection
       
   187 
       
   188     from pyams_utils.adapter import adapter_config
       
   189 
       
   190     @adapter_config(context=IPersistent, provides=ITransactionManager)
       
   191     def get_transaction_manager(obj):
       
   192         conn = IConnection(obj)
       
   193         try:
       
   194             return conn.transaction_manager
       
   195         except AttributeError:
       
   196             return conn._txn_mgr
       
   197 
       
   198 This is another adapter which adapts any contained object to the *IPathElements* interface; this interface can be
       
   199 used to build index that you can use to find objects based on a parent object:
       
   200 
       
   201 .. code-block:: python
       
   202 
       
   203     from pyams_utils.interfaces.traversing import IPathElements
       
   204     from zope.intid.interfaces import IIntIds
       
   205     from zope.location.interfaces import IContained
       
   206 
       
   207     from pyams_utils.adapter import ContextAdapter
       
   208     from pyams_utils.registry import query_utility
       
   209     from pyramid.location import lineage
       
   210 
       
   211     @adapter_config(context=IContained, provides=IPathElements)
       
   212     class PathElementsAdapter(ContextAdapter):
       
   213         """Contained object path elements adapter"""
       
   214 
       
   215         @property
       
   216         def parents(self):
       
   217             intids = query_utility(IIntIds)
       
   218             if intids is None:
       
   219                 return []
       
   220             return [intids.register(parent) for parent in lineage(self.context)]
       
   221 
       
   222 An adapter can also be a multi-adapter, when several input objects are requested to provide a given interface. For
       
   223 example, many adapters require a context and a request, eventually a view, to provide another feature. This is how,
       
   224 for example, we define a custom *name* column in a security manager table displaying a list of plug-ins:
       
   225 
       
   226 .. code-block:: python
       
   227 
       
   228     from pyams_zmi.layer import IAdminLayer
       
   229     from z3c.table.interfaces import IColumn
       
   230 
       
   231     from pyams_skin.table import I18nColumn
       
   232     from z3c.table.column import GetAttrColumn
       
   233 
       
   234     @adapter_config(name='name', context=(Interface, IAdminLayer, SecurityManagerPluginsTable), provides=IColumn)
       
   235     class SecurityManagerPluginsNameColumn(I18nColumn, GetAttrColumn):
       
   236         """Security manager plugins name column"""
       
   237 
       
   238         _header = _("Name")
       
   239         attrName = 'title'
       
   240         weight = 10
       
   241 
       
   242 
       
   243 Registering vocabularies
       
   244 ------------------------
       
   245 
       
   246 A **vocabulary** is a custom factory which can be used as source for several field types, like *choices* or *lists*.
       
   247 Vocabularies have to be registered in a custom registry, so PyAMS_utils provide another annotation to register them.
       
   248 This example is based on the *Timezone* component which allows you to select a timezone between a list of references:
       
   249 
       
   250 .. code-block:: python
       
   251 
       
   252     import pytz
       
   253     from pyams_utils.vocabulary import vocabulary_config
       
   254     from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
       
   255 
       
   256     @vocabulary_config(name='PyAMS timezones')
       
   257     class TimezonesVocabulary(SimpleVocabulary):
       
   258         """Timezones vocabulary"""
       
   259 
       
   260         def __init__(self, *args, **kw):
       
   261             terms = [SimpleTerm(t, t, t) for t in pytz.all_timezones]
       
   262             super(TimezonesVocabulary, self).__init__(terms)