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
=== modified file 'src/lazr/restful/NEWS.txt'
--- src/lazr/restful/NEWS.txt 2010-08-10 12:59:10 +0000
+++ src/lazr/restful/NEWS.txt 2010-08-13 14:46:07 +0000
@@ -2,6 +2,12 @@
2NEWS for lazr.restful2NEWS for lazr.restful
3=====================3=====================
44
50.11.1 (2010-08-13)
6===================
7
8Fixed a bug that prevented first_version_with_total_size_link from
9working properly in a multi-version environment.
10
50.11.0 (2010-08-10)110.11.0 (2010-08-10)
6===================12===================
713
814
=== modified file 'src/lazr/restful/_operation.py'
--- src/lazr/restful/_operation.py 2010-08-09 19:33:20 +0000
+++ src/lazr/restful/_operation.py 2010-08-13 14:46:07 +0000
@@ -4,7 +4,7 @@
44
5import simplejson5import simplejson
66
7from zope.component import getMultiAdapter, queryMultiAdapter7from zope.component import getMultiAdapter, getUtility, queryMultiAdapter
8from zope.event import notify8from zope.event import notify
9from zope.interface import Attribute, implements, providedBy9from zope.interface import Attribute, implements, providedBy
10from zope.interface.interfaces import IInterface10from zope.interface.interfaces import IInterface
@@ -16,10 +16,12 @@
16from lazr.lifecycle.event import ObjectModifiedEvent16from lazr.lifecycle.event import ObjectModifiedEvent
17from lazr.lifecycle.snapshot import Snapshot17from lazr.lifecycle.snapshot import Snapshot
1818
19from lazr.restful.fields import CollectionField
19from lazr.restful.interfaces import (20from lazr.restful.interfaces import (
20 ICollection, IFieldMarshaller, IResourceDELETEOperation,21 ICollection, IFieldMarshaller, IResourceDELETEOperation,
21 IResourceGETOperation, IResourcePOSTOperation)22 IResourceGETOperation, IResourcePOSTOperation, IWebServiceConfiguration)
22from lazr.restful.interfaces import ICollectionField, IReference23from lazr.restful.interfaces import ICollectionField, IReference
24from lazr.restful.utils import is_total_size_link_active
23from lazr.restful._resource import (25from lazr.restful._resource import (
24 BatchingResourceMixin, CollectionResource, ResourceJSONEncoder)26 BatchingResourceMixin, CollectionResource, ResourceJSONEncoder)
2527
@@ -48,7 +50,12 @@
4850
49 def total_size_link(self, navigator):51 def total_size_link(self, navigator):
50 """Return a link to the total size of a collection."""52 """Return a link to the total size of a collection."""
51 if getattr(self, 'include_total_size', True):53 # If the version we're being asked for is equal to or later
54 # than the version in which we started exposing
55 # total_size_link, then include it; otherwise include
56 # total_size.
57 config = getUtility(IWebServiceConfiguration)
58 if not is_total_size_link_active(self.request.version, config):
52 # This is a named operation that includes the total size59 # This is a named operation that includes the total size
53 # inline rather than with a link.60 # inline rather than with a link.
54 return None61 return None
5562
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2010-08-10 18:36:09 +0000
+++ src/lazr/restful/declarations.py 2010-08-13 14:46:07 +0000
@@ -1245,15 +1245,6 @@
1245 '_method_name': method.__name__,1245 '_method_name': method.__name__,
1246 '__doc__': method.__doc__}1246 '__doc__': method.__doc__}
12471247
1248 if isinstance(return_type, CollectionField):
1249 # If the version we're being asked for is equal to or later than the
1250 # version in which we started exposing total_size_link and this is a
1251 # read operation, then include it, otherwise include total_size.
1252 config = getUtility(IWebServiceConfiguration)
1253 class_dict['include_total_size'] = not (
1254 is_total_size_link_active(version, config) and
1255 operation_type == 'read_operation')
1256
1257 if operation_type == 'write_operation':1248 if operation_type == 'write_operation':
1258 class_dict['send_modification_event'] = True1249 class_dict['send_modification_event'] = True
1259 factory = type(name, bases, class_dict)1250 factory = type(name, bases, class_dict)
12601251
=== modified file 'src/lazr/restful/docs/multiversion.txt'
--- src/lazr/restful/docs/multiversion.txt 2010-08-04 18:25:21 +0000
+++ src/lazr/restful/docs/multiversion.txt 2010-08-13 14:46:07 +0000
@@ -40,6 +40,7 @@
40 ... hostname = 'api.multiversion.dev'40 ... hostname = 'api.multiversion.dev'
41 ... use_https = False41 ... use_https = False
42 ... active_versions = ['beta', '1.0', 'dev']42 ... active_versions = ['beta', '1.0', 'dev']
43 ... first_version_with_total_size_link = '1.0'
43 ... code_revision = 'test'44 ... code_revision = 'test'
44 ... max_batch_size = 10045 ... max_batch_size = 100
45 ... view_permission = None46 ... view_permission = None
@@ -52,6 +53,18 @@
52 >>> from zope.component import getUtility53 >>> from zope.component import getUtility
53 >>> config = getUtility(IWebServiceConfiguration)54 >>> config = getUtility(IWebServiceConfiguration)
5455
56Collections previously exposed their total size via a `total_size`
57attribute. However, newer versions of lazr.restful expose a
58`total_size_link` intead. To facilitate transitioning from one
59approach to the other the configuration option
60`first_version_with_total_size_link` has been added to
61IWebServiceConfiguration.
62
63In this case, `first_version_with_total_size_link` is '1.0'. This
64means that named operations in versions prior to '1.0' will always
65return a `total_size`, but named operations in '1.0' and later
66versions will return a `total_size_link` when appropriate.
67
55URL generation68URL generation
56--------------69--------------
5770
@@ -732,7 +745,9 @@
732 >>> print simplejson.loads(field())745 >>> print simplejson.loads(field())
733 111-2121746 111-2121
734747
735We can invoke a named operation.748We can invoke a named operation, and it returns a total_size (because
749'beta' is an earlier version than the
750first_version_with_total_size_link).
736751
737 >>> import simplejson752 >>> import simplejson
738 >>> request_beta = create_web_service_request(753 >>> request_beta = create_web_service_request(
@@ -743,6 +758,9 @@
743 >>> [contact['name'] for contact in result['entries']]758 >>> [contact['name'] for contact in result['entries']]
744 ['Cleo Python']759 ['Cleo Python']
745760
761 >>> result['total_size']
762 1
763
746 >>> request_beta = create_web_service_request(764 >>> request_beta = create_web_service_request(
747 ... '/beta/contact_list',765 ... '/beta/contact_list',
748 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'})766 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'})
@@ -833,7 +851,9 @@
833 NotFound: Object: <Contact object...>, name: u'fax'851 NotFound: Object: <Contact object...>, name: u'fax'
834852
835We can invoke a named operation. Note that the name of the operation853We can invoke a named operation. Note that the name of the operation
836is now 'find' (it was 'findContacts' in 'beta').854is now 'find' (it was 'findContacts' in 'beta'). And note that
855total_size has been replaced by total_size_link, since '1.0' is the
856first_version_with_total_size_link.
837857
838 >>> request_10 = create_web_service_request(858 >>> request_10 = create_web_service_request(
839 ... '/1.0/contacts',859 ... '/1.0/contacts',
@@ -843,6 +863,22 @@
843 >>> [contact['name'] for contact in result['entries']]863 >>> [contact['name'] for contact in result['entries']]
844 ['Cleo Python']864 ['Cleo Python']
845865
866 >>> result['total_size']
867 Traceback (most recent call last):
868 ...
869 KeyError: 'total_size'
870
871 >>> print result['total_size_link']
872 http://.../1.0/contacts?string=Cleo&ws.op=find&ws.show=total_size
873 >>> size_request = create_web_service_request(
874 ... '/1.0/contacts',
875 ... environ={'QUERY_STRING' :
876 ... 'string=Cleo&ws.op=find&ws.show=total_size'})
877 >>> operation = size_request.traverse(None)
878 >>> result = simplejson.loads(operation())
879 >>> print result
880 1
881
846 >>> request_10 = create_web_service_request(882 >>> request_10 = create_web_service_request(
847 ... '/1.0/contacts',883 ... '/1.0/contacts',
848 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})884 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
@@ -952,5 +988,5 @@
952 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})988 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
953 >>> operation = request_dev.traverse(None)989 >>> operation = request_dev.traverse(None)
954 >>> result = simplejson.loads(operation())990 >>> result = simplejson.loads(operation())
955 >>> result['total_size']991 >>> [entry for entry in result['entries']]
956 0992 []
957993
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2010-08-10 11:17:52 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-08-13 14:46:07 +0000
@@ -1651,51 +1651,6 @@
1651 AssertionError: 'IMultiVersionCollection' isn't tagged for export1651 AssertionError: 'IMultiVersionCollection' isn't tagged for export
1652 to web service version 'NoSuchVersion'.1652 to web service version 'NoSuchVersion'.
16531653
1654total_size_link
1655~~~~~~~~~~~~~~~
1656
1657Collections previously exposed their total size via a `total_size` attribute.
1658However, newer versions of lazr.restful expose a `total_size_link` intead. To
1659facilitate transitioning from one approach to the other the configuration
1660option `first_version_with_total_size_link` has been added to
1661IWebServiceConfiguration.
1662
1663By default the first_version_with_total_size_link is set to the earliest
1664available web service version, but if you have stable versions of your web
1665service you wish to maintain compatability with you can specify the version in
1666which you want the new behavior to take effect in the web service
1667configuration.
1668
1669 >>> from zope.component import getUtility
1670 >>> config = getUtility(IWebServiceConfiguration)
1671 >>> config.last_version_with_mutator_named_operations = '1.0'
1672
1673 >>> from lazr.restful.declarations import (
1674 ... export_read_operation, operation_returns_collection_of,
1675 ... operation_for_version)
1676 >>> class IWithMultiVersionCollection(Interface):
1677 ... export_as_webservice_entry()
1678 ...
1679 ... @operation_for_version('2.0')
1680 ... @operation_for_version('1.0')
1681 ... @operation_returns_collection_of(Interface)
1682 ... @export_read_operation()
1683 ... def method():
1684 ... """A method that returns a collection."""
1685
1686 >>> method = IWithMultiVersionCollection['method']
1687 >>> dummy_data = None # this would be an intance that has the method
1688 >>> v10 = generate_operation_adapter(method, '1.0')(dummy_data, request)
1689 >>> v20 = generate_operation_adapter(method, '2.0')(dummy_data, request)
1690
1691We can see that version 1.0 includes the total size for backward compatability
1692while version 2.0 includes a link to fetch the total size.
1693
1694 >>> v10.include_total_size
1695 True
1696 >>> v20.include_total_size
1697 False
1698
1699Entries1654Entries
1700-------1655-------
17011656
17021657
=== modified file 'src/lazr/restful/example/multiversion/resources.py'
--- src/lazr/restful/example/multiversion/resources.py 2010-02-08 18:24:57 +0000
+++ src/lazr/restful/example/multiversion/resources.py 2010-08-13 14:46:07 +0000
@@ -14,7 +14,7 @@
14 export_as_webservice_entry, export_destructor_operation,14 export_as_webservice_entry, export_destructor_operation,
15 export_operation_as, export_read_operation, export_write_operation,15 export_operation_as, export_read_operation, export_write_operation,
16 exported, mutator_for, operation_for_version, operation_parameters,16 exported, mutator_for, operation_for_version, operation_parameters,
17 operation_removed_in_version)17 operation_removed_in_version, operation_returns_collection_of)
1818
19# Our implementations of these classes can be based on the19# Our implementations of these classes can be based on the
20# implementations from the WSGI example.20# implementations from the WSGI example.
@@ -84,6 +84,7 @@
84 # In 1.0 and 2.0, it's published as 'byValue'84 # In 1.0 and 2.0, it's published as 'byValue'
85 @export_operation_as('byValue')85 @export_operation_as('byValue')
86 @operation_parameters(value=Text())86 @operation_parameters(value=Text())
87 @operation_returns_collection_of(IKeyValuePair)
87 @export_read_operation()88 @export_read_operation()
88 @operation_for_version('1.0')89 @operation_for_version('1.0')
89 # This operation is not published in versions earlier than 1.0.90 # This operation is not published in versions earlier than 1.0.
9091
=== modified file 'src/lazr/restful/example/multiversion/root.py'
--- src/lazr/restful/example/multiversion/root.py 2010-02-25 17:07:16 +0000
+++ src/lazr/restful/example/multiversion/root.py 2010-08-13 14:46:07 +0000
@@ -35,6 +35,7 @@
35class WebServiceConfiguration(BaseWSGIWebServiceConfiguration):35class WebServiceConfiguration(BaseWSGIWebServiceConfiguration):
36 code_revision = '1'36 code_revision = '1'
37 active_versions = ['beta', '1.0', '2.0', '3.0', 'trunk']37 active_versions = ['beta', '1.0', '2.0', '3.0', 'trunk']
38 first_version_with_total_size_link = '2.0'
38 last_version_with_mutator_named_operations = '1.0'39 last_version_with_mutator_named_operations = '1.0'
39 use_https = False40 use_https = False
40 view_permission = 'zope.Public'41 view_permission = 'zope.Public'
4142
=== modified file 'src/lazr/restful/example/multiversion/tests/introduction.txt'
--- src/lazr/restful/example/multiversion/tests/introduction.txt 2010-02-10 21:44:13 +0000
+++ src/lazr/restful/example/multiversion/tests/introduction.txt 2010-08-13 14:46:07 +0000
@@ -253,7 +253,7 @@
253 >>> def show_value(version, op):253 >>> def show_value(version, op):
254 ... url = '/pairs?ws.op=%s&value=bar' % op254 ... url = '/pairs?ws.op=%s&value=bar' % op
255 ... body = webservice.get(url, api_version=version).jsonBody()255 ... body = webservice.get(url, api_version=version).jsonBody()
256 ... return body[0]['key']256 ... return body['entries'][0]['key']
257257
258The named operation is not published at all in the 'beta' version of258The named operation is not published at all in the 'beta' version of
259the web service.259the web service.
260260
=== modified file 'src/lazr/restful/example/multiversion/tests/operation.txt'
--- src/lazr/restful/example/multiversion/tests/operation.txt 2010-02-25 21:43:10 +0000
+++ src/lazr/restful/example/multiversion/tests/operation.txt 2010-08-13 14:46:07 +0000
@@ -5,6 +5,46 @@
5Named operations have some special features that are too obscure to5Named operations have some special features that are too obscure to
6mention in the introductory doctest.6mention in the introductory doctest.
77
8total_size versus total_size_link
9---------------------------------
10
11In old versions of lazr.restful, named operations that return
12collections always send a 'total_size' containing the total size of a
13collection.
14
15 >>> from lazr.restful.testing.webservice import WebServiceCaller
16 >>> webservice = WebServiceCaller(domain='multiversion.dev')
17
18In the example web service, named operations always send 'total_size'
19up to version '2.0'.
20
21 >>> from zope.component import getUtility
22 >>> from lazr.restful.interfaces import IWebServiceConfiguration
23 >>> config = getUtility(IWebServiceConfiguration)
24 >>> print config.first_version_with_total_size_link
25 2.0
26
27When the 'byValue' operation is invoked in version 1.0, it returns a
28total_size.
29
30 >>> def get_collection(version, op='byValue'):
31 ... url = '/pairs?ws.op=%s&value=bar' % op
32 ... return webservice.get(url, api_version=version).jsonBody()
33
34 >>> print sorted(get_collection('1.0').keys())
35 [u'entries', u'start', u'total_size']
36
37The operation itself doesn't change between 1.0 and 2.0, but in
38version 2.0, the operation starts returning total_size_link.
39
40 >>> print sorted(get_collection('2.0').keys())
41 [u'entries', u'start', u'total_size_link']
42
43The same happens in 3.0.
44
45 >>> print sorted(get_collection('3.0', 'by_value').keys())
46 [u'entries', u'start', u'total_size_link']
47
8Mutators as named operations48Mutators as named operations
9----------------------------49----------------------------
1050
@@ -12,15 +52,9 @@
12annotated the same way as named operations, and in old versions of52annotated the same way as named operations, and in old versions of
13lazr.restful, they were actually published as named operations.53lazr.restful, they were actually published as named operations.
1454
15 >>> from lazr.restful.testing.webservice import WebServiceCaller
16 >>> webservice = WebServiceCaller(domain='multiversion.dev')
17
18In the example web service, mutator methods are published as named55In the example web service, mutator methods are published as named
19operations in the 'beta' and '1.0' versions.56operations in the 'beta' and '1.0' versions.
2057
21 >>> from zope.component import getUtility
22 >>> from lazr.restful.interfaces import IWebServiceConfiguration
23 >>> config = getUtility(IWebServiceConfiguration)
24 >>> print config.last_version_with_mutator_named_operations58 >>> print config.last_version_with_mutator_named_operations
25 1.059 1.0
2660
2761
=== modified file 'src/lazr/restful/version.txt'
--- src/lazr/restful/version.txt 2010-08-10 12:59:10 +0000
+++ src/lazr/restful/version.txt 2010-08-13 14:46:07 +0000
@@ -1,1 +1,1 @@
10.11.010.11.1

Subscribers

People subscribed via source and target branches