diff -r d7dd088ed557 -r 097b0c025eec src/source/developer_guide/zca.rst --- a/src/source/developer_guide/zca.rst Tue Dec 11 16:43:45 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,292 +0,0 @@ -.. _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 `_ - - `Zope component `_ - - `Zope interface `_ - - -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)