Merge lp:~leonardr/lazr.restful/check_total_size_active_on_call into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: 139
Proposed branch: lp:~leonardr/lazr.restful/check_total_size_active_on_call
Merge into: lp:lazr.restful
Diff against target: 347 lines (+101/-70)
10 files modified
src/lazr/restful/NEWS.txt (+6/-0)
src/lazr/restful/_operation.py (+10/-3)
src/lazr/restful/declarations.py (+0/-9)
src/lazr/restful/docs/multiversion.txt (+40/-4)
src/lazr/restful/docs/webservice-declarations.txt (+0/-45)
src/lazr/restful/example/multiversion/resources.py (+2/-1)
src/lazr/restful/example/multiversion/root.py (+1/-0)
src/lazr/restful/example/multiversion/tests/introduction.txt (+1/-1)
src/lazr/restful/example/multiversion/tests/operation.txt (+40/-6)
src/lazr/restful/version.txt (+1/-1)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/check_total_size_active_on_call
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+32590@code.launchpad.net

Description of the change

The 'first_version_with_total_size_link' configuration setting lets you give named operations one behavior in a later version (only usable by versions of lazr.restfulclient designed to handle it) while maintaining backwards compatible behavior in earlier versions. The behavior is switched on or off by the value of a helper function called is_total_size_link_active().

Previously, is_total_size_link_active() was checked when the method adapter was being generated from annotations. There are two problems with this. First, it means that named operations defined manually don't check is_total_size_link_active() unless you write that code yourself. More seriously, it means that named operations inherit the old behavior, even after 'first_version_with_total_size_link', because method adapters aren't generated for every single version.

Here's a specific example. Consider a Launchpad named operation like 'findPeople'. This operation is defined for 'beta' and never changes. Call this class 'GET_findPeople_beta'. In Launchpad, 'first_version_with_total_size_link' is 'devel'. So you think if you invoked findPeople in the 'devel' web service you'd get the old behavior. But the only adapter method is GET_findPeople_beta. When GET_findPeople_beta was being defined, is_total_size_link_active() returned false because back then, the version under consideration was 'beta'.

So invoking findPeople() will give the same results whether it's done in 'beta' or 'devel', because is_total_size_link_active() was called too early.

This branch fixes this problem by checking is_total_size_link_active() as late as possible--at runtime, right at the point where the behavior needs to diverge depending on whether we want the old behavior or the new behavior. I've added tests for both a web service where everything's defined manually (multiversion.txt) and a web service defined with annotations (examples/multiversion).

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) :
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-08-10 12:59:10 +0000
3+++ src/lazr/restful/NEWS.txt 2010-08-13 14:46:07 +0000
4@@ -2,6 +2,12 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.11.1 (2010-08-13)
9+===================
10+
11+Fixed a bug that prevented first_version_with_total_size_link from
12+working properly in a multi-version environment.
13+
14 0.11.0 (2010-08-10)
15 ===================
16
17
18=== modified file 'src/lazr/restful/_operation.py'
19--- src/lazr/restful/_operation.py 2010-08-09 19:33:20 +0000
20+++ src/lazr/restful/_operation.py 2010-08-13 14:46:07 +0000
21@@ -4,7 +4,7 @@
22
23 import simplejson
24
25-from zope.component import getMultiAdapter, queryMultiAdapter
26+from zope.component import getMultiAdapter, getUtility, queryMultiAdapter
27 from zope.event import notify
28 from zope.interface import Attribute, implements, providedBy
29 from zope.interface.interfaces import IInterface
30@@ -16,10 +16,12 @@
31 from lazr.lifecycle.event import ObjectModifiedEvent
32 from lazr.lifecycle.snapshot import Snapshot
33
34+from lazr.restful.fields import CollectionField
35 from lazr.restful.interfaces import (
36 ICollection, IFieldMarshaller, IResourceDELETEOperation,
37- IResourceGETOperation, IResourcePOSTOperation)
38+ IResourceGETOperation, IResourcePOSTOperation, IWebServiceConfiguration)
39 from lazr.restful.interfaces import ICollectionField, IReference
40+from lazr.restful.utils import is_total_size_link_active
41 from lazr.restful._resource import (
42 BatchingResourceMixin, CollectionResource, ResourceJSONEncoder)
43
44@@ -48,7 +50,12 @@
45
46 def total_size_link(self, navigator):
47 """Return a link to the total size of a collection."""
48- if getattr(self, 'include_total_size', True):
49+ # If the version we're being asked for is equal to or later
50+ # than the version in which we started exposing
51+ # total_size_link, then include it; otherwise include
52+ # total_size.
53+ config = getUtility(IWebServiceConfiguration)
54+ if not is_total_size_link_active(self.request.version, config):
55 # This is a named operation that includes the total size
56 # inline rather than with a link.
57 return None
58
59=== modified file 'src/lazr/restful/declarations.py'
60--- src/lazr/restful/declarations.py 2010-08-10 18:36:09 +0000
61+++ src/lazr/restful/declarations.py 2010-08-13 14:46:07 +0000
62@@ -1245,15 +1245,6 @@
63 '_method_name': method.__name__,
64 '__doc__': method.__doc__}
65
66- if isinstance(return_type, CollectionField):
67- # If the version we're being asked for is equal to or later than the
68- # version in which we started exposing total_size_link and this is a
69- # read operation, then include it, otherwise include total_size.
70- config = getUtility(IWebServiceConfiguration)
71- class_dict['include_total_size'] = not (
72- is_total_size_link_active(version, config) and
73- operation_type == 'read_operation')
74-
75 if operation_type == 'write_operation':
76 class_dict['send_modification_event'] = True
77 factory = type(name, bases, class_dict)
78
79=== modified file 'src/lazr/restful/docs/multiversion.txt'
80--- src/lazr/restful/docs/multiversion.txt 2010-08-04 18:25:21 +0000
81+++ src/lazr/restful/docs/multiversion.txt 2010-08-13 14:46:07 +0000
82@@ -40,6 +40,7 @@
83 ... hostname = 'api.multiversion.dev'
84 ... use_https = False
85 ... active_versions = ['beta', '1.0', 'dev']
86+ ... first_version_with_total_size_link = '1.0'
87 ... code_revision = 'test'
88 ... max_batch_size = 100
89 ... view_permission = None
90@@ -52,6 +53,18 @@
91 >>> from zope.component import getUtility
92 >>> config = getUtility(IWebServiceConfiguration)
93
94+Collections previously exposed their total size via a `total_size`
95+attribute. However, newer versions of lazr.restful expose a
96+`total_size_link` intead. To facilitate transitioning from one
97+approach to the other the configuration option
98+`first_version_with_total_size_link` has been added to
99+IWebServiceConfiguration.
100+
101+In this case, `first_version_with_total_size_link` is '1.0'. This
102+means that named operations in versions prior to '1.0' will always
103+return a `total_size`, but named operations in '1.0' and later
104+versions will return a `total_size_link` when appropriate.
105+
106 URL generation
107 --------------
108
109@@ -732,7 +745,9 @@
110 >>> print simplejson.loads(field())
111 111-2121
112
113-We can invoke a named operation.
114+We can invoke a named operation, and it returns a total_size (because
115+'beta' is an earlier version than the
116+first_version_with_total_size_link).
117
118 >>> import simplejson
119 >>> request_beta = create_web_service_request(
120@@ -743,6 +758,9 @@
121 >>> [contact['name'] for contact in result['entries']]
122 ['Cleo Python']
123
124+ >>> result['total_size']
125+ 1
126+
127 >>> request_beta = create_web_service_request(
128 ... '/beta/contact_list',
129 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'})
130@@ -833,7 +851,9 @@
131 NotFound: Object: <Contact object...>, name: u'fax'
132
133 We can invoke a named operation. Note that the name of the operation
134-is now 'find' (it was 'findContacts' in 'beta').
135+is now 'find' (it was 'findContacts' in 'beta'). And note that
136+total_size has been replaced by total_size_link, since '1.0' is the
137+first_version_with_total_size_link.
138
139 >>> request_10 = create_web_service_request(
140 ... '/1.0/contacts',
141@@ -843,6 +863,22 @@
142 >>> [contact['name'] for contact in result['entries']]
143 ['Cleo Python']
144
145+ >>> result['total_size']
146+ Traceback (most recent call last):
147+ ...
148+ KeyError: 'total_size'
149+
150+ >>> print result['total_size_link']
151+ http://.../1.0/contacts?string=Cleo&ws.op=find&ws.show=total_size
152+ >>> size_request = create_web_service_request(
153+ ... '/1.0/contacts',
154+ ... environ={'QUERY_STRING' :
155+ ... 'string=Cleo&ws.op=find&ws.show=total_size'})
156+ >>> operation = size_request.traverse(None)
157+ >>> result = simplejson.loads(operation())
158+ >>> print result
159+ 1
160+
161 >>> request_10 = create_web_service_request(
162 ... '/1.0/contacts',
163 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
164@@ -952,5 +988,5 @@
165 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
166 >>> operation = request_dev.traverse(None)
167 >>> result = simplejson.loads(operation())
168- >>> result['total_size']
169- 0
170+ >>> [entry for entry in result['entries']]
171+ []
172
173=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
174--- src/lazr/restful/docs/webservice-declarations.txt 2010-08-10 11:17:52 +0000
175+++ src/lazr/restful/docs/webservice-declarations.txt 2010-08-13 14:46:07 +0000
176@@ -1651,51 +1651,6 @@
177 AssertionError: 'IMultiVersionCollection' isn't tagged for export
178 to web service version 'NoSuchVersion'.
179
180-total_size_link
181-~~~~~~~~~~~~~~~
182-
183-Collections previously exposed their total size via a `total_size` attribute.
184-However, newer versions of lazr.restful expose a `total_size_link` intead. To
185-facilitate transitioning from one approach to the other the configuration
186-option `first_version_with_total_size_link` has been added to
187-IWebServiceConfiguration.
188-
189-By default the first_version_with_total_size_link is set to the earliest
190-available web service version, but if you have stable versions of your web
191-service you wish to maintain compatability with you can specify the version in
192-which you want the new behavior to take effect in the web service
193-configuration.
194-
195- >>> from zope.component import getUtility
196- >>> config = getUtility(IWebServiceConfiguration)
197- >>> config.last_version_with_mutator_named_operations = '1.0'
198-
199- >>> from lazr.restful.declarations import (
200- ... export_read_operation, operation_returns_collection_of,
201- ... operation_for_version)
202- >>> class IWithMultiVersionCollection(Interface):
203- ... export_as_webservice_entry()
204- ...
205- ... @operation_for_version('2.0')
206- ... @operation_for_version('1.0')
207- ... @operation_returns_collection_of(Interface)
208- ... @export_read_operation()
209- ... def method():
210- ... """A method that returns a collection."""
211-
212- >>> method = IWithMultiVersionCollection['method']
213- >>> dummy_data = None # this would be an intance that has the method
214- >>> v10 = generate_operation_adapter(method, '1.0')(dummy_data, request)
215- >>> v20 = generate_operation_adapter(method, '2.0')(dummy_data, request)
216-
217-We can see that version 1.0 includes the total size for backward compatability
218-while version 2.0 includes a link to fetch the total size.
219-
220- >>> v10.include_total_size
221- True
222- >>> v20.include_total_size
223- False
224-
225 Entries
226 -------
227
228
229=== modified file 'src/lazr/restful/example/multiversion/resources.py'
230--- src/lazr/restful/example/multiversion/resources.py 2010-02-08 18:24:57 +0000
231+++ src/lazr/restful/example/multiversion/resources.py 2010-08-13 14:46:07 +0000
232@@ -14,7 +14,7 @@
233 export_as_webservice_entry, export_destructor_operation,
234 export_operation_as, export_read_operation, export_write_operation,
235 exported, mutator_for, operation_for_version, operation_parameters,
236- operation_removed_in_version)
237+ operation_removed_in_version, operation_returns_collection_of)
238
239 # Our implementations of these classes can be based on the
240 # implementations from the WSGI example.
241@@ -84,6 +84,7 @@
242 # In 1.0 and 2.0, it's published as 'byValue'
243 @export_operation_as('byValue')
244 @operation_parameters(value=Text())
245+ @operation_returns_collection_of(IKeyValuePair)
246 @export_read_operation()
247 @operation_for_version('1.0')
248 # This operation is not published in versions earlier than 1.0.
249
250=== modified file 'src/lazr/restful/example/multiversion/root.py'
251--- src/lazr/restful/example/multiversion/root.py 2010-02-25 17:07:16 +0000
252+++ src/lazr/restful/example/multiversion/root.py 2010-08-13 14:46:07 +0000
253@@ -35,6 +35,7 @@
254 class WebServiceConfiguration(BaseWSGIWebServiceConfiguration):
255 code_revision = '1'
256 active_versions = ['beta', '1.0', '2.0', '3.0', 'trunk']
257+ first_version_with_total_size_link = '2.0'
258 last_version_with_mutator_named_operations = '1.0'
259 use_https = False
260 view_permission = 'zope.Public'
261
262=== modified file 'src/lazr/restful/example/multiversion/tests/introduction.txt'
263--- src/lazr/restful/example/multiversion/tests/introduction.txt 2010-02-10 21:44:13 +0000
264+++ src/lazr/restful/example/multiversion/tests/introduction.txt 2010-08-13 14:46:07 +0000
265@@ -253,7 +253,7 @@
266 >>> def show_value(version, op):
267 ... url = '/pairs?ws.op=%s&value=bar' % op
268 ... body = webservice.get(url, api_version=version).jsonBody()
269- ... return body[0]['key']
270+ ... return body['entries'][0]['key']
271
272 The named operation is not published at all in the 'beta' version of
273 the web service.
274
275=== modified file 'src/lazr/restful/example/multiversion/tests/operation.txt'
276--- src/lazr/restful/example/multiversion/tests/operation.txt 2010-02-25 21:43:10 +0000
277+++ src/lazr/restful/example/multiversion/tests/operation.txt 2010-08-13 14:46:07 +0000
278@@ -5,6 +5,46 @@
279 Named operations have some special features that are too obscure to
280 mention in the introductory doctest.
281
282+total_size versus total_size_link
283+---------------------------------
284+
285+In old versions of lazr.restful, named operations that return
286+collections always send a 'total_size' containing the total size of a
287+collection.
288+
289+ >>> from lazr.restful.testing.webservice import WebServiceCaller
290+ >>> webservice = WebServiceCaller(domain='multiversion.dev')
291+
292+In the example web service, named operations always send 'total_size'
293+up to version '2.0'.
294+
295+ >>> from zope.component import getUtility
296+ >>> from lazr.restful.interfaces import IWebServiceConfiguration
297+ >>> config = getUtility(IWebServiceConfiguration)
298+ >>> print config.first_version_with_total_size_link
299+ 2.0
300+
301+When the 'byValue' operation is invoked in version 1.0, it returns a
302+total_size.
303+
304+ >>> def get_collection(version, op='byValue'):
305+ ... url = '/pairs?ws.op=%s&value=bar' % op
306+ ... return webservice.get(url, api_version=version).jsonBody()
307+
308+ >>> print sorted(get_collection('1.0').keys())
309+ [u'entries', u'start', u'total_size']
310+
311+The operation itself doesn't change between 1.0 and 2.0, but in
312+version 2.0, the operation starts returning total_size_link.
313+
314+ >>> print sorted(get_collection('2.0').keys())
315+ [u'entries', u'start', u'total_size_link']
316+
317+The same happens in 3.0.
318+
319+ >>> print sorted(get_collection('3.0', 'by_value').keys())
320+ [u'entries', u'start', u'total_size_link']
321+
322 Mutators as named operations
323 ----------------------------
324
325@@ -12,15 +52,9 @@
326 annotated the same way as named operations, and in old versions of
327 lazr.restful, they were actually published as named operations.
328
329- >>> from lazr.restful.testing.webservice import WebServiceCaller
330- >>> webservice = WebServiceCaller(domain='multiversion.dev')
331-
332 In the example web service, mutator methods are published as named
333 operations in the 'beta' and '1.0' versions.
334
335- >>> from zope.component import getUtility
336- >>> from lazr.restful.interfaces import IWebServiceConfiguration
337- >>> config = getUtility(IWebServiceConfiguration)
338 >>> print config.last_version_with_mutator_named_operations
339 1.0
340
341
342=== modified file 'src/lazr/restful/version.txt'
343--- src/lazr/restful/version.txt 2010-08-10 12:59:10 +0000
344+++ src/lazr/restful/version.txt 2010-08-13 14:46:07 +0000
345@@ -1,1 +1,1 @@
346-0.11.0
347+0.11.1

Subscribers

People subscribed via source and target branches