Merge lp:~leonardr/lazr.restful/mutators-are-not-named-operations into lp:lazr.restful
- mutators-are-not-named-operations
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/mutators-are-not-named-operations |
Merge into: | lp:lazr.restful |
Diff against target: |
415 lines (+246/-4) 11 files modified
src/lazr/restful/NEWS.txt (+13/-0) src/lazr/restful/declarations.py (+1/-0) src/lazr/restful/docs/absoluteurl.txt (+1/-0) src/lazr/restful/docs/webservice-declarations.txt (+170/-3) src/lazr/restful/docs/webservice-error.txt (+1/-0) src/lazr/restful/example/base/root.py (+1/-0) src/lazr/restful/example/multiversion/root.py (+1/-0) src/lazr/restful/example/wsgi/root.py (+1/-0) src/lazr/restful/interfaces/_rest.py (+12/-0) src/lazr/restful/metazcml.py (+44/-1) src/lazr/restful/tests/test_webservice.py (+1/-0) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/mutators-are-not-named-operations |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eleanor Berger (community) | code | Approve | |
Review via email: mp+20180@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
Revision history for this message
Eleanor Berger (intellectronica) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/lazr/restful/NEWS.txt' |
2 | --- src/lazr/restful/NEWS.txt 2010-02-23 13:26:11 +0000 |
3 | +++ src/lazr/restful/NEWS.txt 2010-02-25 21:46:13 +0000 |
4 | @@ -2,6 +2,19 @@ |
5 | NEWS for lazr.restful |
6 | ===================== |
7 | |
8 | +Development |
9 | +=========== |
10 | + |
11 | +Special note: this version will break backwards compatibility in your |
12 | +web service unless you take a special step. See |
13 | +"last_version_with_named_mutator_operations" below. |
14 | + |
15 | +By default, mutator methods are no longer separately published as |
16 | +named operations. To maintain backwards compatibility (or if you just |
17 | +want this feature back), put the name of the most recent version of |
18 | +your web service in the "last_version_with_named_mutator_operations" |
19 | +field of your IWebServiceConfiguration implementation. |
20 | + |
21 | 0.9.21 (2010-02-23) |
22 | =================== |
23 | |
24 | |
25 | === modified file 'src/lazr/restful/declarations.py' |
26 | --- src/lazr/restful/declarations.py 2010-02-23 17:26:37 +0000 |
27 | +++ src/lazr/restful/declarations.py 2010-02-25 21:46:13 +0000 |
28 | @@ -561,6 +561,7 @@ |
29 | "A field can only have one mutator method for version %s; " |
30 | "%s makes two." % (_version_name(version), method.__name__ )) |
31 | mutator_annotations[version] = (method, dict(method_annotations)) |
32 | + method_annotations['is_mutator'] = True |
33 | |
34 | |
35 | def _free_parameters(method, annotations): |
36 | |
37 | === modified file 'src/lazr/restful/docs/absoluteurl.txt' |
38 | --- src/lazr/restful/docs/absoluteurl.txt 2010-02-11 17:57:16 +0000 |
39 | +++ src/lazr/restful/docs/absoluteurl.txt 2010-02-25 21:46:13 +0000 |
40 | @@ -30,6 +30,7 @@ |
41 | ... hostname = "hostname" |
42 | ... service_root_uri_prefix = "root_uri_prefix/" |
43 | ... active_versions = ['active_version', 'latest_version'] |
44 | + ... last_version_with_mutator_named_operations = None |
45 | ... port = 1000 |
46 | ... use_https = True |
47 | |
48 | |
49 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' |
50 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-02-23 17:26:37 +0000 |
51 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-02-25 21:46:13 +0000 |
52 | @@ -903,6 +903,7 @@ |
53 | ... implements(IWebServiceConfiguration) |
54 | ... view_permission = "lazr.View" |
55 | ... active_versions = ["beta", "1.0", "2.0", "3.0"] |
56 | + ... last_version_with_mutator_named_operations = "1.0" |
57 | ... code_revision = "1.0b" |
58 | ... default_batch_size = 50 |
59 | ... |
60 | @@ -1259,7 +1260,9 @@ |
61 | DELETE_IBookOnSteroids_destroy_beta |
62 | |
63 | |
64 | -=== Destructor === |
65 | + |
66 | +Destructor |
67 | +========== |
68 | |
69 | A method can be designated as a destructor for the entry. Here, the |
70 | destroy() method is designated as the destructor for IHasText. |
71 | @@ -2265,8 +2268,8 @@ |
72 | >>> attrs['return_type'] |
73 | <lazr.restful.fields.CollectionField object...> |
74 | |
75 | -Mutator operations |
76 | -****************** |
77 | +Mutators |
78 | +******** |
79 | |
80 | Different versions can define different mutator methods for the same field. |
81 | |
82 | @@ -2617,3 +2620,167 @@ |
83 | annotations for version "2.0", even though it's not published in |
84 | that version. The bad annotations are: "params", "as". |
85 | ... |
86 | + |
87 | +Mutators as named operations |
88 | +---------------------------- |
89 | + |
90 | +In earlier versions of lazr.restful, mutator methods were published as |
91 | +named operations. This behavior is now deprecated and will eventually |
92 | +be removed. But to maintain backwards compatibility, mutator methods |
93 | +are still published as named operations up to a certain point. The |
94 | +MyWebServiceConfiguration class (above) defines |
95 | +last_version_with_mutator_named_operations as '1.0', meaning that in |
96 | +'beta' and '1.0', mutator methods will be published as named |
97 | +operations, and in '2.0' and '3.0' they will not. |
98 | + |
99 | +Let's consider an entry that defines a mutator in the very first |
100 | +version of the web service and never removes it. |
101 | + |
102 | + >>> class IBetaMutatorEntry(Interface): |
103 | + ... export_as_webservice_entry() |
104 | + ... |
105 | + ... field = exported(TextLine(readonly=True)) |
106 | + ... |
107 | + ... @mutator_for(field) |
108 | + ... @export_write_operation() |
109 | + ... @operation_parameters(new_value=TextLine()) |
110 | + ... def set_value(new_value): |
111 | + ... pass |
112 | + |
113 | + >>> class BetaMutator: |
114 | + ... implements(IBetaMutatorEntry) |
115 | + |
116 | + >>> module = register_test_module( |
117 | + ... 'betamutator', IBetaMutatorEntry, BetaMutator) |
118 | + |
119 | +Here's a helper method that will look up named operation for a given |
120 | +version. |
121 | + |
122 | + >>> from zope.interface import alsoProvides |
123 | + >>> from lazr.restful.interfaces import IResourcePOSTOperation |
124 | + >>> def operation_for(context, version, name): |
125 | + ... request = FakeRequest(version=version) |
126 | + ... marker = getUtility(IWebServiceVersion, name=version) |
127 | + ... alsoProvides(request, marker) |
128 | + ... return getMultiAdapter( |
129 | + ... (context, request), IResourcePOSTOperation, name) |
130 | + |
131 | +In the 'beta' and '1.0' versions, the lookup succeeds and returns the |
132 | +generated adapter class defined for 'beta'. These two versions publish |
133 | +"set_value" as a named POST operation. |
134 | + |
135 | + >>> context = BetaMutator() |
136 | + >>> operation_for(context, 'beta', 'set_value') |
137 | + <lazr.restful.declarations.POST_IBetaMutatorEntry_set_value_beta ...> |
138 | + >>> operation_for(context, '1.0', 'set_value') |
139 | + <lazr.restful.declarations.POST_IBetaMutatorEntry_set_value_beta ...> |
140 | + |
141 | +In '2.0', the lookup fails, not because of anything in the definition |
142 | +of IBetaMutatorEntry, but because the web service configuration |
143 | +defines 1.0 as the last version in which mutators are published as |
144 | +named operations. |
145 | + |
146 | + >>> operation_for(context, '2.0', 'set_value') |
147 | + Traceback (most recent call last): |
148 | + ... |
149 | + ComponentLookupError: ... |
150 | + |
151 | +Here's an entry that defines a mutator method in version 2.0, after |
152 | +the cutoff point. |
153 | + |
154 | + >>> class I20MutatorEntry(Interface): |
155 | + ... export_as_webservice_entry() |
156 | + ... |
157 | + ... field = exported(TextLine(readonly=True)) |
158 | + ... |
159 | + ... @mutator_for(field) |
160 | + ... @export_write_operation() |
161 | + ... @operation_parameters(new_value=TextLine()) |
162 | + ... @operation_for_version('2.0') |
163 | + ... def set_value(new_value): |
164 | + ... pass |
165 | + |
166 | + >>> class Mutator20: |
167 | + ... implements(I20MutatorEntry) |
168 | + |
169 | + >>> module = register_test_module( |
170 | + ... 'mutator20', I20MutatorEntry, Mutator20) |
171 | + |
172 | +The named operation lookup never succeeds. In '1.0' it fails because |
173 | +the mutator hasn't been published yet. In '2.0' it fails because that |
174 | +version comes after the last one to publish mutators as named |
175 | +operations ('1.0'). |
176 | + |
177 | + >>> context = Mutator20() |
178 | + >>> operation_for(context, '1.0', 'set_value') |
179 | + Traceback (most recent call last): |
180 | + ... |
181 | + ComponentLookupError: ... |
182 | + |
183 | + >>> operation_for(context, '2.0', 'set_value') |
184 | + Traceback (most recent call last): |
185 | + ... |
186 | + ComponentLookupError: ... |
187 | + |
188 | +Here's one that shows a limitation of the software. This method |
189 | +defines a mutator 'set_value' for version 1.0, which will be removed |
190 | +in version 2.0. It *also* defines a named operation to be published |
191 | +as 'set_value' in version 2.0, and a third operation to be published |
192 | +as 'set_value' in version 3.0. |
193 | + |
194 | + >>> class IMutatorPlusNamedOperationEntry(Interface): |
195 | + ... export_as_webservice_entry() |
196 | + ... |
197 | + ... field = exported(TextLine(readonly=True)) |
198 | + ... |
199 | + ... @mutator_for(field) |
200 | + ... @export_write_operation() |
201 | + ... @operation_parameters(new_value=TextLine()) |
202 | + ... @operation_for_version('1.0') |
203 | + ... def set_value(new_value): |
204 | + ... pass |
205 | + ... |
206 | + ... @export_write_operation() |
207 | + ... @operation_parameters(new_value=TextLine()) |
208 | + ... @export_operation_as('set_value') |
209 | + ... @operation_for_version('2.0') |
210 | + ... def not_a_mutator(new_value): |
211 | + ... pass |
212 | + ... |
213 | + ... @export_write_operation() |
214 | + ... @operation_parameters(new_value=TextLine()) |
215 | + ... @export_operation_as('set_value') |
216 | + ... @operation_for_version('3.0') |
217 | + ... def also_not_a_mutator(new_value): |
218 | + ... pass |
219 | + |
220 | + >>> class MutatorPlusNamedOperation: |
221 | + ... implements(IMutatorPlusNamedOperationEntry) |
222 | + |
223 | + >>> module = register_test_module( |
224 | + ... 'multimutator', IMutatorPlusNamedOperationEntry, |
225 | + ... MutatorPlusNamedOperation) |
226 | + |
227 | +The mutator is accessible for version 1.0, as you'd expect. |
228 | + |
229 | + >>> context = MutatorPlusNamedOperation() |
230 | + >>> print operation_for(context, '1.0', 'set_value').__name__ |
231 | + POST_IMutatorPlusNamedOperationEntry_set_value_1_0 |
232 | + |
233 | +But the named operation that replaces the mutator in version 1.0 is |
234 | +not accessible. |
235 | + |
236 | + >>> operation_for(context, '2.0', 'set_value') |
237 | + Traceback (most recent call last): |
238 | + ... |
239 | + ComponentLookupError: ... |
240 | + |
241 | +The named operation of the same name defined in version 3.0 _is_ |
242 | +accessible. |
243 | + |
244 | + >>> print operation_for(context, '3.0', 'set_value').__name__ |
245 | + POST_IMutatorPlusNamedOperationEntry_set_value_3_0 |
246 | + |
247 | +So, in the version that gets rid of named operations for mutator |
248 | +methods, you can't define a named operation with the same name as one |
249 | +of the outgoing mutator methods. |
250 | |
251 | === modified file 'src/lazr/restful/docs/webservice-error.txt' |
252 | --- src/lazr/restful/docs/webservice-error.txt 2010-02-11 17:57:16 +0000 |
253 | +++ src/lazr/restful/docs/webservice-error.txt 2010-02-25 21:46:13 +0000 |
254 | @@ -17,6 +17,7 @@ |
255 | ... implements(IWebServiceConfiguration) |
256 | ... show_tracebacks = False |
257 | ... active_versions = ['trunk'] |
258 | + ... last_version_with_mutator_named_operations = None |
259 | >>> webservice_configuration = SimpleWebServiceConfiguration() |
260 | >>> sm.registerUtility(webservice_configuration) |
261 | |
262 | |
263 | === modified file 'src/lazr/restful/example/base/root.py' |
264 | --- src/lazr/restful/example/base/root.py 2010-02-18 15:02:10 +0000 |
265 | +++ src/lazr/restful/example/base/root.py 2010-02-25 21:46:13 +0000 |
266 | @@ -390,6 +390,7 @@ |
267 | hostname='cookbooks.dev' |
268 | match_batch_size=50 |
269 | active_versions=['1.0', 'devel'] |
270 | + last_version_with_mutator_named_operations=None |
271 | use_https=False |
272 | view_permission='lazr.restful.example.base.View' |
273 | |
274 | |
275 | === modified file 'src/lazr/restful/example/multiversion/root.py' |
276 | --- src/lazr/restful/example/multiversion/root.py 2010-02-11 17:57:16 +0000 |
277 | +++ src/lazr/restful/example/multiversion/root.py 2010-02-25 21:46:13 +0000 |
278 | @@ -35,6 +35,7 @@ |
279 | class WebServiceConfiguration(BaseWSGIWebServiceConfiguration): |
280 | code_revision = '1' |
281 | active_versions = ['beta', '1.0', '2.0', '3.0', 'trunk'] |
282 | + last_version_with_mutator_named_operations = '1.0' |
283 | use_https = False |
284 | view_permission = 'zope.Public' |
285 | |
286 | |
287 | === modified file 'src/lazr/restful/example/wsgi/root.py' |
288 | --- src/lazr/restful/example/wsgi/root.py 2009-11-12 19:08:10 +0000 |
289 | +++ src/lazr/restful/example/wsgi/root.py 2010-02-25 21:46:13 +0000 |
290 | @@ -36,6 +36,7 @@ |
291 | code_revision = '1' |
292 | active_versions = ['1.0'] |
293 | use_https = False |
294 | + last_version_with_mutator_named_operations = None |
295 | view_permission = 'zope.Public' |
296 | |
297 | |
298 | |
299 | === modified file 'src/lazr/restful/interfaces/_rest.py' |
300 | --- src/lazr/restful/interfaces/_rest.py 2010-02-11 17:57:16 +0000 |
301 | +++ src/lazr/restful/interfaces/_rest.py 2010-02-25 21:46:13 +0000 |
302 | @@ -448,6 +448,18 @@ |
303 | |
304 | This list must contain at least one version name.""") |
305 | |
306 | + last_version_with_mutator_named_operations = TextLine( |
307 | + default=None, |
308 | + description=u"""In earlier versions of lazr.restful, mutator methods |
309 | + were also published as named operations. This redundant |
310 | + behavior is no longer enabled by default, but this setting |
311 | + allows for backwards compatibility. |
312 | + |
313 | + Mutator methods will also be published as named operations in |
314 | + the version you specify here, and in any previous versions. In |
315 | + all subsequent versions, they will not be published as named |
316 | + operations.""") |
317 | + |
318 | code_revision = TextLine( |
319 | default=u"", |
320 | description=u"""A string designating the current revision |
321 | |
322 | === modified file 'src/lazr/restful/metazcml.py' |
323 | --- src/lazr/restful/metazcml.py 2010-02-23 17:26:37 +0000 |
324 | +++ src/lazr/restful/metazcml.py 2010-02-25 21:46:13 +0000 |
325 | @@ -213,6 +213,19 @@ |
326 | Different versions of the web service may publish the same |
327 | operation differently or under different names. |
328 | """ |
329 | + # First of all, figure out when to stop publishing field mutators |
330 | + # as named operations. |
331 | + config = getUtility(IWebServiceConfiguration) |
332 | + if config.last_version_with_mutator_named_operations is None: |
333 | + no_mutator_operations_after = None |
334 | + block_mutator_operations_as_of_version = None |
335 | + else: |
336 | + no_mutator_operations_after = config.active_versions.index( |
337 | + config.last_version_with_mutator_named_operations) |
338 | + if len(config.active_versions) > no_mutator_operations_after: |
339 | + block_mutator_operations_as_of_version = config.active_versions[ |
340 | + no_mutator_operations_after+1] |
341 | + |
342 | for name, method in interface.namesAndDescriptions(True): |
343 | tag = method.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) |
344 | if tag is None or tag['type'] not in OPERATION_TYPES: |
345 | @@ -237,6 +250,7 @@ |
346 | # version. |
347 | previous_operation_name = None |
348 | previous_operation_provides = None |
349 | + operation_registered_as_mutator = False |
350 | for version, tag in tag.stack: |
351 | if tag['type'] == REMOVED_OPERATION_TYPE: |
352 | # This operation is not present in this version. |
353 | @@ -284,10 +298,29 @@ |
354 | # that case is handled above. |
355 | raise AssertionError( |
356 | 'Unknown operation type: %s' % tag['type']) |
357 | + |
358 | operation_name = tag.get('as') |
359 | if tag['type'] in ['destructor']: |
360 | operation_name = '' |
361 | - factory = generate_operation_adapter(method, version) |
362 | + |
363 | + if version is None: |
364 | + this_version_index = 0 |
365 | + else: |
366 | + this_version_index = config.active_versions.index( |
367 | + version) |
368 | + if (tag.get('is_mutator', False) |
369 | + and no_mutator_operations_after < this_version_index): |
370 | + # This is a mutator method, and in this version, |
371 | + # mutator methods are not published as named |
372 | + # operations at all. Block any lookup of the named |
373 | + # operation from succeeding. |
374 | + # |
375 | + # This will save us from having to do another |
376 | + # de-registration later. |
377 | + factory = _mask_adapter_registration |
378 | + operation_registered_as_mutator = False |
379 | + else: |
380 | + factory = generate_operation_adapter(method, version) |
381 | |
382 | # Operations are looked up by name. If the operation's |
383 | # name has changed from the previous version to this |
384 | @@ -307,9 +340,19 @@ |
385 | register_adapter_for_version( |
386 | factory, interface, version, operation_provides, |
387 | operation_name, context.info) |
388 | + if tag.get('is_mutator'): |
389 | + operation_registered_as_mutator = True |
390 | previous_operation_name = operation_name |
391 | previous_operation_provides = operation_provides |
392 | |
393 | + if operation_registered_as_mutator: |
394 | + # The operation was registered as a mutator, and it never |
395 | + # got de-registered. De-register it now. |
396 | + register_adapter_for_version( |
397 | + _mask_adapter_registration, interface, |
398 | + block_mutator_operations_as_of_version, |
399 | + previous_operation_provides, previous_operation_name, |
400 | + context.info) |
401 | |
402 | def _mask_adapter_registration(*args): |
403 | """A factory function that stops an adapter lookup from succeeding. |
404 | |
405 | === modified file 'src/lazr/restful/tests/test_webservice.py' |
406 | --- src/lazr/restful/tests/test_webservice.py 2010-02-18 17:33:04 +0000 |
407 | +++ src/lazr/restful/tests/test_webservice.py 2010-02-25 21:46:13 +0000 |
408 | @@ -114,6 +114,7 @@ |
409 | show_tracebacks = False |
410 | active_versions = ['1.0', '2.0'] |
411 | hostname = "webservice_test" |
412 | + last_version_with_mutator_named_operations = None |
413 | |
414 | def createRequest(self, body_instream, environ): |
415 | request = Request(body_instream, environ) |
This branch stops mutator methods from being published as named operations. To preserve backwards compatibility, users are allowed to specify in which version of the web service the change takes effect.
The tests in webservice- declarations. txt and the new example/ multiversion/ tests/operation .txt should explain the feature. webservice- declarations. txt also walks you through one place where the feature might cause someone pain (it's very unlikely, and difficult to fix, so I left it alone).