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