Merge lp:~leonardr/lazr.restful/mutators-are-not-named-operations into lp:lazr.restful

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
Reviewer Review Type Date Requested Status
Eleanor Berger (community) code Approve
Review via email: mp+20180@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

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

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)

Subscribers

People subscribed via source and target branches