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) |
|