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