Merge lp:~leonardr/lazr.restful/enforce-version-order into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/enforce-version-order
Merge into: lp:lazr.restful
Diff against target: 256 lines (+159/-24)
3 files modified
src/lazr/restful/docs/webservice-declarations.txt (+85/-22)
src/lazr/restful/metazcml.py (+72/-0)
src/lazr/restful/simple.py (+2/-2)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/enforce-version-order
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Review via email: mp+18223@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch adds two bits of multiversion-related error checking.

1. Named operations are annotated with a stack of version information. The most recent version is supposed to be at the top of the stack.

This is right:

@operation_for_version("2.0")
...
@operation_for_version("1.0")
...
@operation_for_version("beta")

This is wrong:

@operation_for_version("2.0")
...
@operation_for_version("beta")
...
@operation_for_version("1.0")

The ensure_correct_version_ordering function looks up the canonical version ordering in the IWebServiceConfiguration implementation, and makes sure every named operation is annotated with a consistent ordering. If the developer makes a mistake, ensure_correct_version_ordering raises an AssertionError explaining the problem and giving the correct ordering.

This code has to run in the second stage of ZCML processing, rather than when the annotations are initially processed, because until the IWebServiceConfiguration utility becomes available, there's no way to know whether version "foo" comes before or after version "bar".

2. If a named operation is not published in a certain version, it makes no sense to annotate it for that version.

@export_operation_as("its_gone_in_1_0") <--This annotation is stupid!
@operation_removed_in_version("1.0")
@export_as_read_operation()

The register_webservice_operations method will now make sure there are no extra annotations for a version in which a named operation is not published. If there are any extra annotations, it will raise an AssertionError and complain about them.

This code has to run in the second stage of ZCML processing because by then all the annotations have been processed. If I make each annotations in charge of complaining when it's applied to an unpublished method, it'll reject cases in which the method seems unpublished at the time but gets published later.

@export_as_read_operation()
@export_operation_as("its_redefined_in_1_0") <--This looks stupid, but turns out to be valid!
@operation_removed_in_version("1.0")
@export_as_read_operation()

This set of annotations is sloppier than this one:

@export_operation_as("its_redefined_in_1_0") <--This is clearly valid!
@export_as_read_operation()
@operation_removed_in_version("1.0")
@export_as_read_operation()

But that's not worth raising an error about.

Revision history for this message
Leonard Richardson (leonardr) wrote :

I should mention that I refactored some code in webservice-declarations.txt that creates a module out of web service classes and then runs lazr.restful's ZCML processing on that module, so that I could test these two cases.

108. By Leonard Richardson

Clarified explanation in doctest.:

109. By Leonard Richardson

Changed 'after' to 'on top' to fit better with the stack metaphor.

Revision history for this message
Paul Hummer (rockstar) wrote :

Hi Leonard-

  Sorry this took so long. I ended up having to look at a lot more code than what is in the patch. This lazr.restful stuff is a bit more involved than I thought. These changes all look good though. Please land it.

Cheers,
Paul

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2010-01-28 14:21:19 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-28 18:11:15 +0000
@@ -888,7 +888,7 @@
888 >>> class MyWebServiceConfiguration:888 >>> class MyWebServiceConfiguration:
889 ... implements(IWebServiceConfiguration)889 ... implements(IWebServiceConfiguration)
890 ... view_permission = "lazr.View"890 ... view_permission = "lazr.View"
891 ... active_versions = ["beta"]891 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
892 ... code_revision = "1.0b"892 ... code_revision = "1.0b"
893 ... default_batch_size = 50893 ... default_batch_size = 50
894 ... latest_version_uri_prefix = 'beta'894 ... latest_version_uri_prefix = 'beta'
@@ -1855,11 +1855,7 @@
1855 <lazr.restful.fields.CollectionField object...>1855 <lazr.restful.fields.CollectionField object...>
18561856
1857[XXX leonardr mutator_for modifies the field, not the method, so it1857[XXX leonardr mutator_for modifies the field, not the method, so it
1858won't work until I add multiversion support for fields. Also, it's1858won't work until I add multiversion support for fields.]
1859possible to remove a named operation from a certain version and then
1860uselessly annotate it some more. The best place to catch this error
1861turns out to be when generating the adapter classes, not when
1862collecting annotations.]
18631859
1864Security1860Security
1865========1861========
@@ -1895,7 +1891,6 @@
1895 zope.security._proxy._Proxy (using zope.security.checker.Checker)1891 zope.security._proxy._Proxy (using zope.security.checker.Checker)
1896 public: __call__, send_modification_event1892 public: __call__, send_modification_event
18971893
1898=================
1899ZCML Registration1894ZCML Registration
1900=================1895=================
19011896
@@ -1908,23 +1903,34 @@
19081903
1909 >>> import sys1904 >>> import sys
1910 >>> from types import ModuleType1905 >>> from types import ModuleType
1911 >>> bookexample = ModuleType('bookexample')1906 >>> def create_test_module(name, *contents):
1912 >>> sys.modules['lazr.restful.bookexample'] = bookexample1907 ... """Defines a new module and adds it to sys.modules."""
1913 >>> bookexample.IBook = IBook1908 ... new_module = ModuleType(name)
1914 >>> bookexample.IBookSet = IBookSet1909 ... sys.modules['lazr.restful.' + name] = new_module
1915 >>> bookexample.IBookOnSteroids = IBookOnSteroids1910 ... for object in contents:
1916 >>> bookexample.IBookSetOnSteroids = IBookSetOnSteroids1911 ... setattr(new_module, object.__name__, object)
1917 >>> bookexample.ISimpleComment = ISimpleComment1912 ... return new_module
1918 >>> bookexample.InvalidEmail = InvalidEmail
19191913
1920 >>> from zope.configuration import xmlconfig1914 >>> from zope.configuration import xmlconfig
1921 >>> zcmlcontext = xmlconfig.string("""1915 >>> def register_test_module(name, *contents):
1922 ... <configure1916 ... new_module = create_test_module(name, *contents)
1923 ... xmlns:webservice="http://namespaces.canonical.com/webservice">1917 ... try:
1924 ... <include package="lazr.restful" file="meta.zcml" />1918 ... zcmlcontext = xmlconfig.string("""
1925 ... <webservice:register module="lazr.restful.bookexample" />1919 ... <configure
1926 ... </configure>1920 ... xmlns:webservice=
1927 ... """)1921 ... "http://namespaces.canonical.com/webservice">
1922 ... <include package="lazr.restful" file="meta.zcml" />
1923 ... <webservice:register module="lazr.restful.%s" />
1924 ... </configure>
1925 ... """ % name)
1926 ... except Exception, e:
1927 ... del sys.modules['lazr.restful.' + name]
1928 ... raise e
1929 ... return new_module
1930
1931 >>> bookexample = register_test_module(
1932 ... 'bookexample', IBook, IBookSet, IBookOnSteroids,
1933 ... IBookSetOnSteroids, ISimpleComment, InvalidEmail)
19281934
1929After the registration, adapters from IBook to IEntry, and IBookSet to1935After the registration, adapters from IBook to IEntry, and IBookSet to
1930ICollection are available:1936ICollection are available:
@@ -1980,3 +1986,60 @@
19801986
1981 >>> del bookexample1987 >>> del bookexample
1982 >>> del sys.modules['lazr.restful.bookexample']1988 >>> del sys.modules['lazr.restful.bookexample']
1989
1990Error handling
1991--------------
1992
1993Some error handling happens in the ZCML registration phase. At this
1994point, all the annotations have been processed, and the
1995IWebServiceConfiguration utility (with its canonical list of versions)
1996has become available. This lets us run checks on the versioning
1997annotations that couldn't be run before.
1998
1999Here's a class annotated by someone who believes that version 1.0 of
2000the web service is a later version than version 2.0. (Or who believes
2001that named operation annotations proceed from the top down rather than
2002the bottom up.)
2003
2004 >>> class WrongOrderVersions(Interface):
2005 ... export_as_webservice_entry()
2006 ... @export_operation_as('10_name')
2007 ... @operation_for_version("1.0")
2008 ... @operation_parameters(arg=Float())
2009 ... @export_read_operation()
2010 ... @operation_for_version("2.0")
2011 ... def method(arg):
2012 ... """A method."""
2013
2014An attempt to register this module with ZCML results in an error
2015explaining the problem.
2016
2017 >>> register_test_module('wrongorder', WrongOrderVersions)
2018 Traceback (most recent call last):
2019 ...
2020 ConfigurationExecutionError: <type 'exceptions.AssertionError'>:
2021 Annotations on "WrongOrderVersions.method" put an earlier version
2022 on top of a later version: "beta", "2.0", "1.0". The correct order
2023 is: "beta", "1.0", "2.0". ...
2024
2025Here's a class in which a named operation is removed in version 1.0
2026and then annotated without being reinstated.
2027
2028 >>> class AnnotatingARemovedMethod(Interface):
2029 ... export_as_webservice_entry()
2030 ... @operation_parameters(arg=TextLine())
2031 ... @export_operation_as('already_been_removed')
2032 ... @operation_removed_in_version("1.0")
2033 ... @operation_parameters(arg=Float())
2034 ... @export_read_operation()
2035 ... @operation_for_version("2.0")
2036 ... def method(arg):
2037 ... """A method."""
2038
2039 >>> register_test_module('annotatingremoved', AnnotatingARemovedMethod)
2040 Traceback (most recent call last):
2041 ...
2042 ZopeXMLConfigurationError: ...
2043 AssertionError: Method "method" contains annotations for version
2044 "1.0", even though it's not published in that version. The bad
2045 annotations are: "params", "as".
19832046
=== modified file 'src/lazr/restful/metazcml.py'
--- src/lazr/restful/metazcml.py 2010-01-27 17:33:32 +0000
+++ src/lazr/restful/metazcml.py 2010-01-28 18:11:15 +0000
@@ -34,6 +34,50 @@
34 title=u'Module which will be inspected for webservice declarations')34 title=u'Module which will be inspected for webservice declarations')
3535
3636
37def ensure_correct_version_ordering(name, version_list):
38 """Make sure that a list mentions versions from earliest to latest.
39
40 If an earlier version shows up after a later version, this is a
41 sign that a developer was confused about version names when
42 annotating the web service.
43
44 :param name: The name of the object annotated with the
45 possibly mis-ordered versions.
46
47 :param version_list: The list of versions found in the interface.
48
49 :raise AssertionError: If the given version list is not a
50 earlier-to-later ordering of a subset of the web service's
51 versions.
52 """
53 configuration = getUtility(IWebServiceConfiguration)
54 actual_versions = configuration.active_versions + [
55 configuration.latest_version_uri_prefix]
56 # Replace None with the actual version number of the earliest
57 # version.
58 try:
59 earliest_version_pos = version_list.index(None)
60 version_list = list(version_list)
61 version_list[earliest_version_pos] = actual_versions[0]
62 except ValueError:
63 # The earliest version was not mentioned in the version list.
64 # Do nothing.
65 pass
66
67 # Sort version_list according to the list of actual versions.
68 # If the sorted list is different from the unsorted list, at
69 # least one version is out of place.
70 def compare(x, y):
71 return cmp (actual_versions.index(x), actual_versions.index(y))
72 sorted_version_list = sorted(version_list, cmp=compare)
73 if sorted_version_list != version_list:
74 bad_versions = '", "'.join(version_list)
75 good_versions = '", "'.join(sorted_version_list)
76 msg = ('Annotations on "%s" put an earlier version on top of a '
77 'later version: "%s". The correct order is: "%s".')
78 raise AssertionError, msg % (name, bad_versions, good_versions)
79
80
37def register_adapter_for_version(factory, interface, version_name,81def register_adapter_for_version(factory, interface, version_name,
38 provides, name, info):82 provides, name, info):
39 """A version-aware wrapper for the registerAdapter operation.83 """A version-aware wrapper for the registerAdapter operation.
@@ -147,6 +191,14 @@
147 # This method is not published as a named operation.191 # This method is not published as a named operation.
148 continue192 continue
149193
194 # First, make sure that this method was annotated with the
195 # versions in the right order.
196 context.action(
197 discriminator=('webservice version ordering', method),
198 callable=ensure_correct_version_ordering,
199 args=(interface.__name__ + '.' + method.__name__, tag.dict_names)
200 )
201
150 operation_name = None202 operation_name = None
151 # If an operation's name does not change between version n and203 # If an operation's name does not change between version n and
152 # version n+1, we want lookups for requests that come in for204 # version n+1, we want lookups for requests that come in for
@@ -175,6 +227,26 @@
175 operation_name = None227 operation_name = None
176 operation_provides = None228 operation_provides = None
177 factory = None229 factory = None
230
231 # If there are any other tags besides 'type', it means
232 # that the developer tried to annotate a method for a
233 # version where the method isn't published. Let's warn
234 # them about it.
235 #
236 # (We can't make this check when the annotation
237 # happens, because it will reject a method that uses
238 # an annotation like @export_operation_as before using
239 # an annotation like @export_read_operation. That's a
240 # little sloppy, but it's not bad enough to warrant an
241 # exception.)
242 tag_names = list(tag.keys())
243 if tag_names != ['type']:
244 tag_names.remove('type')
245 raise AssertionError(
246 'Method "%s" contains annotations for version "%s", '
247 'even though it\'s not published in that version. '
248 'The bad annotations are: "%s".' % (
249 method.__name__, version, '", "'.join(tag_names)))
178 else:250 else:
179 if tag['type'] == 'read_operation':251 if tag['type'] == 'read_operation':
180 operation_provides = IResourceGETOperation252 operation_provides = IResourceGETOperation
181253
=== modified file 'src/lazr/restful/simple.py'
--- src/lazr/restful/simple.py 2010-01-12 15:06:15 +0000
+++ src/lazr/restful/simple.py 2010-01-28 18:11:15 +0000
@@ -32,8 +32,8 @@
3232
33from lazr.restful import EntryAdapterUtility, ServiceRootResource33from lazr.restful import EntryAdapterUtility, ServiceRootResource
34from lazr.restful.interfaces import (34from lazr.restful.interfaces import (
35 IServiceRootResource, ITraverseWithGet, IWebServiceConfiguration,35 IServiceRootResource, ITopLevelEntryLink, ITraverseWithGet,
36 IWebServiceLayer)36 IWebServiceConfiguration, IWebServiceLayer)
37from lazr.restful.publisher import (37from lazr.restful.publisher import (
38 WebServicePublicationMixin, WebServiceRequestTraversal)38 WebServicePublicationMixin, WebServiceRequestTraversal)
39from lazr.restful.utils import implement_from_dict39from lazr.restful.utils import implement_from_dict

Subscribers

People subscribed via source and target branches