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