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