Merge lp:~leonardr/lazr.restful/must-specify-version into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Aaron Bentley
Approved revision: 191
Merged at revision: 180
Proposed branch: lp:~leonardr/lazr.restful/must-specify-version
Merge into: lp:lazr.restful
Diff against target: 742 lines (+326/-138)
6 files modified
src/lazr/restful/declarations.py (+70/-37)
src/lazr/restful/docs/webservice-declarations.txt (+50/-45)
src/lazr/restful/interfaces/_rest.py (+7/-0)
src/lazr/restful/testing/helpers.py (+1/-0)
src/lazr/restful/testing/webservice.py (+19/-7)
src/lazr/restful/tests/test_declarations.py (+179/-49)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/must-specify-version
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+52705@code.launchpad.net

Description of the change

This is the first in a series of branches that will let us be much stricter about how the multi-version Launchpad web service is defined. Basically, we will set 'require_explicit_versions' in our web service configuration. This will make it impossible to publish an entry, a collection, a field, or a named operation without explicitly specifying in which version of the web service it first appears.

This first branch implements require_explicit_versions for the fields of entries. Basically, if require_explicit_versions is set, these common calls to exported() will give an error:

field = exported(Field())
field = exported(Field(), exported_as='a_field')
field = exported(Field(), ('2.0', exported_as='a_field'))

You must explicitly state which version is the first to contain this field, by using the 'as_of' keyword argument to exported():

field = exported(Field(), as_of='2.0')
field = exported(Field(), exported_as='a_field', as_of='beta')
field = exported(Field(), ('2.0', exported_as='a_field'), as_of='1.0')

= Effect on keyword arguments to exported() =

Previously, the keyword arguments such as 'as' affected the first version of the web service. We don't know the name of this version when exported() is called, because the IWebServiceConfiguration (which contains that information) hasn't been registered yet.

With 'as_of' in place, keyword arguments such as 'as' affect the first _published_ version of the web service, and we do know the name of that version--it's the value of 'as_of'. This lets us skip a confusing consolidation step later on, when we have both an implicit and an explicit definition of the web service.

The code for the consolidation step has been refactored to make it clearer (because I originally thought I'd have to use it), but its functionality should not have changed. I left the clearer code in place because, well, it's clearer.

= Test case refactoring =

A big chunk of code is me refactoring some (but not all) web service setup from WebServiceTestCase into the new class TestCaseWithWebServiceFixtures. This is useful in unit tests that need to test how web service interfaces and adapters are generated, but that don't need to actually set up a running web service.

I used this class in my new TestRequireExplicitVersions class, and changed the existing ContributingInterfacesTestCase to subclass this class instead of doing its own setup. This in turn required changing those tests to use the versions '1.0' and '2.0' (the ones defined by TestCaseWithWebServiceFixtures) instead of 'beta' and '1.0'.

As noted in the code, I couldn't get testtools.TestCase to play nicely with zope's Cleanup class, so I ended up writing a little helper to simulate testtools' version of assertRaises (which returns the raised exception so you can make more assertions about it).

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I still think it would be nicer to allow a default to be provided for exported_as, per our IRC discussion, but it's not a deal-breaker.

There are some extra blank lines at 669 and 679, and it would be nice to document every test.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2011-03-02 20:08:58 +0000
+++ src/lazr/restful/declarations.py 2011-03-09 16:02:29 +0000
@@ -212,17 +212,14 @@
212 The dictionary may contain the key 'exported', which controls212 The dictionary may contain the key 'exported', which controls
213 whether or not to publish this field at all in the given213 whether or not to publish this field at all in the given
214 version, and the key 'exported_as', which controls the name to214 version, and the key 'exported_as', which controls the name to
215 use when publishing the field. as=None means to use the215 use when publishing the field. exported_as=None means to use
216 field's internal name.216 the field's internal name.
217
218 :param as_of: The name of the earliest version to contain this field.
217219
218 :param exported_as: the name under which the field is published in220 :param exported_as: the name under which the field is published in
219 the entry in the earliest version of the web service. By221 the entry the first time it shows up (ie. in the 'as_of'
220 default, the field's internal name is used. This is a simpler222 version). By default, the field's internal name is used.
221 alternative to the 'versioned_annotations' parameter, for fields
222 whose names don't change in different versions.
223
224 :param exported: Whether or not the field should be published in
225 the earliest version of the web service.
226223
227 :raises TypeError: if called on an object which doesn't provide IField.224 :raises TypeError: if called on an object which doesn't provide IField.
228 :returns: The field with an added tagged value.225 :returns: The field with an added tagged value.
@@ -233,24 +230,37 @@
233 if IObject.providedBy(field) and not IReference.providedBy(field):230 if IObject.providedBy(field) and not IReference.providedBy(field):
234 raise TypeError("Object exported; use Reference instead.")231 raise TypeError("Object exported; use Reference instead.")
235232
236 # The first step is to turn the arguments into a233 # The first step is to turn the arguments into a VersionedDict
237 # VersionedDict. We'll start by collecting annotations for the234 # describing the different ways this field is exposed in different
238 # earliest version, which we refer to as None because we don't235 # versions.
239 # know any of the real version strings yet.
240 annotation_stack = VersionedDict()236 annotation_stack = VersionedDict()
241 annotation_stack.push(None)237 first_version_name = kwparams.pop('as_of', None)
238 annotation_stack.push(first_version_name)
242 annotation_stack['type'] = FIELD_TYPE239 annotation_stack['type'] = FIELD_TYPE
243240
241 if first_version_name is not None:
242 # The user explicitly said to start publishing this field in a
243 # particular version.
244 annotation_stack['_as_of_was_used'] = True
245 annotation_stack['exported'] = True
246
244 annotation_key_for_argument_key = {'exported_as' : 'as',247 annotation_key_for_argument_key = {'exported_as' : 'as',
245 'exported' : 'exported',248 'exported' : 'exported',
246 'readonly' : 'readonly'}249 'readonly' : 'readonly'}
247250
248 # If keyword parameters are present, they define the field's251 # If keyword parameters are present, they define the field's
249 # behavior for the first version. Incorporate them into the252 # behavior for the first exposed version. Incorporate them into
250 # VersionedDict.253 # the VersionedDict.
251 for (key, annotation_key) in annotation_key_for_argument_key.items():254 for (key, annotation_key) in annotation_key_for_argument_key.items():
252 if key in kwparams:255 if key in kwparams:
256 if (key == "exported" and kwparams[key] == False
257 and first_version_name is not None):
258 raise ValueError(
259 ("as_of=%s says to export %s, but exported=False "
260 "says not to.") % (
261 first_version_name, field.__class__.__name__))
253 annotation_stack[annotation_key] = kwparams.pop(key)262 annotation_stack[annotation_key] = kwparams.pop(key)
263
254 # If any keywords are left over, raise an exception.264 # If any keywords are left over, raise an exception.
255 if len(kwparams) > 0:265 if len(kwparams) > 0:
256 raise TypeError("exported got an unexpected keyword "266 raise TypeError("exported got an unexpected keyword "
@@ -275,7 +285,7 @@
275 # We track the field's mutator information separately because it's285 # We track the field's mutator information separately because it's
276 # defined in the named operations, not in the fields. The last286 # defined in the named operations, not in the fields. The last
277 # thing we want to do is try to insert a foreign value into an287 # thing we want to do is try to insert a foreign value into an
278 # already create annotation stack.288 # already created annotation stack.
279 field.setTaggedValue(LAZR_WEBSERVICE_MUTATORS, {})289 field.setTaggedValue(LAZR_WEBSERVICE_MUTATORS, {})
280290
281 return field291 return field
@@ -1320,27 +1330,37 @@
1320 versioned_dict = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)1330 versioned_dict = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
1321 mutator_annotations = field.queryTaggedValue(LAZR_WEBSERVICE_MUTATORS)1331 mutator_annotations = field.queryTaggedValue(LAZR_WEBSERVICE_MUTATORS)
13221332
1323 # If the first version is None and the second version is the
1324 # earliest version, consolidate the two versions.
1325 earliest_version = versions[0]1333 earliest_version = versions[0]
1326 stack = versioned_dict.stack1334 stack = versioned_dict.stack
13271335
1328 if (len(stack) >= 21336 if (len(stack) >= 2 and stack[0][0] is None
1329 and stack[0][0] is None and stack[1][0] == earliest_version):1337 and stack[1][0] == earliest_version):
13301338 # The behavior of the earliest version is defined with keyword
1331 # Make sure the implicit definition of the earliest version1339 # arguments, but the first explicitly-defined version also
1332 # doesn't conflict with the explicit definition. The only1340 # refers to the earliest version. We need to consolidate the
1333 # exception is the 'as' value, which is implicitly defined1341 # versions.
1334 # by the system, not explicitly by the user.1342 implicit_earliest_version = stack[0][1]
1335 for key, value in stack[1][1].items():1343 explicit_earliest_version = stack[1][1]
1336 implicit_value = stack[0][1].get(key)1344 for key, value in explicit_earliest_version.items():
1337 if (implicit_value != value and1345 if key not in implicit_earliest_version:
1338 not (key == 'as' and implicit_value == field.__name__)):1346 # This key was defined for the earliest version using a
1339 raise ValueError(1347 # configuration dictionary, but not defined at all
1340 error_prefix + 'Annotation "%s" has conflicting values '1348 # using keyword arguments. The configuration
1341 'for the earliest version: "%s" (from keyword arguments) '1349 # dictionary takes precedence.
1342 'and "%s" (defined explicitly).' % (1350 continue
1343 key, implicit_value, value))1351 implicit_value = implicit_earliest_version[key]
1352 if implicit_value == value:
1353 # The two values are in sync.
1354 continue
1355 if key == 'as' and implicit_value == field.__name__:
1356 # The implicit value was set by the system, not by the
1357 # user. The later value will simply take precedence.
1358 continue
1359 raise ValueError(
1360 error_prefix + 'Annotation "%s" has conflicting values '
1361 'for the earliest version: "%s" (from keyword arguments) '
1362 'and "%s" (defined explicitly).' % (
1363 key, implicit_value, value))
1344 stack[0][1].update(stack[1][1])1364 stack[0][1].update(stack[1][1])
1345 stack.remove(stack[1])1365 stack.remove(stack[1])
13461366
@@ -1349,6 +1369,19 @@
1349 if stack[0][0] is None:1369 if stack[0][0] is None:
1350 stack[0] = (earliest_version, stack[0][1])1370 stack[0] = (earliest_version, stack[0][1])
13511371
1372 # If require_explicit_versions is set, make sure the first version
1373 # to set 'exported' also sets '_as_of_was_used'.
1374 if getUtility(IWebServiceConfiguration).require_explicit_versions:
1375 for version, annotations in stack:
1376 if annotations.get('exported', False):
1377 if not annotations.get('_as_of_was_used', False):
1378 raise ValueError(
1379 error_prefix + (
1380 "Field was exported in version %s, but not"
1381 " by using as_of. The service configuration"
1382 " requires that you use as_of." % version))
1383 break
1384
1352 # Make sure there is at most one mutator for the earliest version.1385 # Make sure there is at most one mutator for the earliest version.
1353 # If there is one, move it from the mutator-specific dictionary to1386 # If there is one, move it from the mutator-specific dictionary to
1354 # the normal tag stack.1387 # the normal tag stack.
@@ -1386,8 +1419,8 @@
1386 if stack[0][0] == earliest_version:1419 if stack[0][0] == earliest_version:
1387 new_stack = [stack[0]]1420 new_stack = [stack[0]]
1388 else:1421 else:
1389 # The field is not initially published.1422 # The field is not initially exported.
1390 new_stack = (earliest_version, dict(published=False))1423 new_stack = [(earliest_version, dict(exported=False))]
1391 most_recent_tags = new_stack[0][1]1424 most_recent_tags = new_stack[0][1]
1392 most_recent_mutator_tags = earliest_mutator1425 most_recent_mutator_tags = earliest_mutator
1393 for version in versions[1:]:1426 for version in versions[1:]:
13941427
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2011-02-16 15:41:04 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2011-03-09 16:02:29 +0000
@@ -847,6 +847,55 @@
847Generating the webservice847Generating the webservice
848=========================848=========================
849849
850Setup
851-----
852
853Before we can continue, we must define a web service configuration
854object. Each web service needs to have one of these registered
855utilities providing basic information about the web service. This one
856is just a dummy.
857
858 >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
859 >>> from zope.component import provideUtility
860 >>> from lazr.restful.interfaces import IWebServiceConfiguration
861 >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
862 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
863 ... last_version_with_mutator_named_operations = "1.0"
864 ... first_version_with_total_size_link = "2.0"
865 ... code_revision = "1.0b"
866 ... default_batch_size = 50
867 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
868
869We must also set up the ability to create versioned requests. This web
870service has four versions: 'beta', '1.0', '2.0', and '3.0'. We'll
871need a marker interface for every version, registered as a utility
872under the name of the version.
873
874Each version interface subclasses the previous version's
875interface. This lets a request use a resource definition for the
876previous version if it hasn't changed since then.
877
878 >>> from zope.component import getSiteManager
879 >>> from lazr.restful.interfaces import IWebServiceVersion
880 >>> class ITestServiceRequestBeta(IWebServiceVersion):
881 ... pass
882 >>> class ITestServiceRequest10(ITestServiceRequestBeta):
883 ... pass
884 >>> class ITestServiceRequest20(ITestServiceRequest10):
885 ... pass
886 >>> class ITestServiceRequest30(ITestServiceRequest20):
887 ... pass
888 >>> sm = getSiteManager()
889 >>> for marker, name in [(ITestServiceRequestBeta, 'beta'),
890 ... (ITestServiceRequest10, '1.0'),
891 ... (ITestServiceRequest20, '2.0'),
892 ... (ITestServiceRequest30, '3.0')]:
893 ... sm.registerUtility(marker, IWebServiceVersion, name=name)
894
895 >>> from lazr.restful.testing.webservice import FakeRequest
896 >>> request = FakeRequest(version='beta')
897
898
850Entry899Entry
851-----900-----
852901
@@ -968,51 +1017,6 @@
968 ... self.base_price = base_price1017 ... self.base_price = base_price
969 ... self.inventory_number = inventory_number1018 ... self.inventory_number = inventory_number
9701019
971Before we can continue, we must define a web service configuration
972object. Each web service needs to have one of these registered
973utilities providing basic information about the web service. This one
974is just a dummy.
975
976 >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
977 >>> from zope.component import provideUtility
978 >>> from lazr.restful.interfaces import IWebServiceConfiguration
979 >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
980 ... active_versions = ["beta", "1.0", "2.0", "3.0"]
981 ... last_version_with_mutator_named_operations = "1.0"
982 ... first_version_with_total_size_link = "2.0"
983 ... code_revision = "1.0b"
984 ... default_batch_size = 50
985 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
986
987We must also set up the ability to create versioned requests. This web
988service has four versions: 'beta', '1.0', '2.0', and '3.0'. We'll
989need a marker interface for every version, registered as a utility
990under the name of the version.
991
992Each version interface subclasses the previous version's
993interface. This lets a request use a resource definition for the
994previous version if it hasn't changed since then.
995
996 >>> from zope.component import getSiteManager
997 >>> from lazr.restful.interfaces import IWebServiceVersion
998 >>> class ITestServiceRequestBeta(IWebServiceVersion):
999 ... pass
1000 >>> class ITestServiceRequest10(ITestServiceRequestBeta):
1001 ... pass
1002 >>> class ITestServiceRequest20(ITestServiceRequest10):
1003 ... pass
1004 >>> class ITestServiceRequest30(ITestServiceRequest20):
1005 ... pass
1006 >>> sm = getSiteManager()
1007 >>> for marker, name in [(ITestServiceRequestBeta, 'beta'),
1008 ... (ITestServiceRequest10, '1.0'),
1009 ... (ITestServiceRequest20, '2.0'),
1010 ... (ITestServiceRequest30, '3.0')]:
1011 ... sm.registerUtility(marker, IWebServiceVersion, name=name)
1012
1013 >>> from lazr.restful.testing.webservice import FakeRequest
1014 >>> request = FakeRequest(version='beta')
1015
1016Now we can turn a Book object into something that implements1020Now we can turn a Book object into something that implements
1017IBookEntry.1021IBookEntry.
10181022
@@ -1047,6 +1051,7 @@
1047 ...1051 ...
1048 TypeError: 'IBookSet' isn't exported as an entry.1052 TypeError: 'IBookSet' isn't exported as an entry.
10491053
1054
1050Collection1055Collection
1051----------1056----------
10521057
10531058
=== modified file 'src/lazr/restful/interfaces/_rest.py'
--- src/lazr/restful/interfaces/_rest.py 2011-03-02 18:49:23 +0000
+++ src/lazr/restful/interfaces/_rest.py 2011-03-09 16:02:29 +0000
@@ -514,6 +514,13 @@
514 default = {}514 default = {}
515 )515 )
516516
517 require_explicit_versions = Bool(
518 default=False,
519 description=u"""If true, each exported field and named
520 operation must explicitly declare the first version in which
521 it appears. If false, fields and operations are published in
522 all versions.""")
523
517 last_version_with_mutator_named_operations = TextLine(524 last_version_with_mutator_named_operations = TextLine(
518 default=None,525 default=None,
519 description=u"""In earlier versions of lazr.restful, mutator methods526 description=u"""In earlier versions of lazr.restful, mutator methods
520527
=== modified file 'src/lazr/restful/testing/helpers.py'
--- src/lazr/restful/testing/helpers.py 2011-02-17 14:56:25 +0000
+++ src/lazr/restful/testing/helpers.py 2011-03-09 16:02:29 +0000
@@ -70,6 +70,7 @@
70 code_revision = "1.0b"70 code_revision = "1.0b"
71 default_batch_size = 5071 default_batch_size = 50
72 hostname = 'example.com'72 hostname = 'example.com'
73 require_explicit_versions = False
7374
74 def get_request_user(self):75 def get_request_user(self):
75 return 'A user'76 return 'A user'
7677
=== modified file 'src/lazr/restful/testing/webservice.py'
--- src/lazr/restful/testing/webservice.py 2011-02-17 14:56:25 +0000
+++ src/lazr/restful/testing/webservice.py 2011-03-09 16:02:29 +0000
@@ -28,8 +28,9 @@
28import simplejson28import simplejson
29import sys29import sys
30from types import ModuleType30from types import ModuleType
31import unittest
31import urllib32import urllib
32import unittest33
33from urlparse import urljoin34from urlparse import urljoin
34import wsgi_intercept35import wsgi_intercept
3536
@@ -448,15 +449,15 @@
448 """A marker interface for requests to the '2.0' web service."""449 """A marker interface for requests to the '2.0' web service."""
449450
450451
451class WebServiceTestCase(CleanUp, unittest.TestCase):452class TestCaseWithWebServiceFixtures(CleanUp, unittest.TestCase):
452 """A test case for web service operations."""453 """A test case that needs to have some aspects of a web service set up.
453454
454 testmodule_objects = []455 If the test case is testing a real web service, use
456 WebServiceTestCase instead.
457 """
455458
456 def setUp(self):459 def setUp(self):
457 """Set the component registry with the given model."""460 super(TestCaseWithWebServiceFixtures, self).setUp()
458 super(WebServiceTestCase, self).setUp()
459
460 # Register a simple configuration object.461 # Register a simple configuration object.
461 webservice_configuration = WebServiceTestConfiguration()462 webservice_configuration = WebServiceTestConfiguration()
462 sm = getGlobalSiteManager()463 sm = getGlobalSiteManager()
@@ -471,7 +472,18 @@
471 sm.registerUtility(472 sm.registerUtility(
472 IWebServiceTestRequest20, IWebServiceVersion, name='2.0')473 IWebServiceTestRequest20, IWebServiceVersion, name='2.0')
473474
475
476class WebServiceTestCase(TestCaseWithWebServiceFixtures):
477 """A test case for web service operations."""
478
479 testmodule_objects = []
480
481 def setUp(self):
482 """Set the component registry with the given model."""
483 super(WebServiceTestCase, self).setUp()
484
474 # Register a service root resource485 # Register a service root resource
486 sm = getGlobalSiteManager()
475 service_root = ServiceRootResource()487 service_root = ServiceRootResource()
476 sm.registerUtility(service_root, IServiceRootResource)488 sm.registerUtility(service_root, IServiceRootResource)
477489
478490
=== modified file 'src/lazr/restful/tests/test_declarations.py'
--- src/lazr/restful/tests/test_declarations.py 2011-01-21 21:08:43 +0000
+++ src/lazr/restful/tests/test_declarations.py 2011-03-09 16:02:29 +0000
@@ -1,5 +1,3 @@
1import unittest
2
3from zope.component import (1from zope.component import (
4 adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)2 adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)
5from zope.component.interfaces import ComponentLookupError3from zope.component.interfaces import ComponentLookupError
@@ -10,9 +8,15 @@
10from zope.security.management import endInteraction, newInteraction8from zope.security.management import endInteraction, newInteraction
119
12from lazr.restful.declarations import (10from lazr.restful.declarations import (
13 export_as_webservice_entry, exported, export_read_operation,11 export_as_webservice_entry,
14 export_write_operation, mutator_for, operation_for_version,12 exported,
15 operation_parameters)13 export_read_operation,
14 export_write_operation,
15 generate_entry_interfaces,
16 mutator_for,
17 operation_for_version,
18 operation_parameters,
19 )
16from lazr.restful.fields import Reference20from lazr.restful.fields import Reference
17from lazr.restful.interfaces import (21from lazr.restful.interfaces import (
18 IEntry, IResourceGETOperation, IWebServiceConfiguration,22 IEntry, IResourceGETOperation, IWebServiceConfiguration,
@@ -22,32 +26,30 @@
22 AttemptToContributeToNonExportedInterface,26 AttemptToContributeToNonExportedInterface,
23 ConflictInContributingInterfaces, find_interfaces_and_contributors)27 ConflictInContributingInterfaces, find_interfaces_and_contributors)
24from lazr.restful._resource import EntryAdapterUtility, EntryResource28from lazr.restful._resource import EntryAdapterUtility, EntryResource
25from lazr.restful.testing.webservice import FakeRequest29from lazr.restful.testing.webservice import (
30 FakeRequest,
31 TestCaseWithWebServiceFixtures,
32 )
26from lazr.restful.testing.helpers import (33from lazr.restful.testing.helpers import (
27 create_test_module, TestWebServiceConfiguration, register_test_module)34 create_test_module, TestWebServiceConfiguration, register_test_module)
2835
2936
30class ContributingInterfacesTestCase(unittest.TestCase):37class ContributingInterfacesTestCase(TestCaseWithWebServiceFixtures):
31 """Tests for interfaces that contribute fields/operations to others."""38 """Tests for interfaces that contribute fields/operations to others."""
3239
33 def setUp(self):40 def setUp(self):
34 provideUtility(41 super(ContributingInterfacesTestCase, self).setUp()
35 TestWebServiceConfiguration(), IWebServiceConfiguration)
36 sm = getSiteManager()42 sm = getSiteManager()
37 sm.registerUtility(
38 ITestServiceRequestBeta, IWebServiceVersion, name='beta')
39 sm.registerUtility(
40 ITestServiceRequest10, IWebServiceVersion, name='1.0')
41 sm.registerAdapter(ProductToHasBugsAdapter)43 sm.registerAdapter(ProductToHasBugsAdapter)
42 sm.registerAdapter(ProjectToHasBugsAdapter)44 sm.registerAdapter(ProjectToHasBugsAdapter)
43 sm.registerAdapter(ProductToHasBranchesAdapter)45 sm.registerAdapter(ProductToHasBranchesAdapter)
44 sm.registerAdapter(DummyFieldMarshaller)46 sm.registerAdapter(DummyFieldMarshaller)
45 self.beta_request = FakeRequest(version='beta')
46 alsoProvides(
47 self.beta_request, getUtility(IWebServiceVersion, name='beta'))
48 self.one_zero_request = FakeRequest(version='1.0')47 self.one_zero_request = FakeRequest(version='1.0')
49 alsoProvides(48 alsoProvides(
50 self.one_zero_request, getUtility(IWebServiceVersion, name='1.0'))49 self.one_zero_request, getUtility(IWebServiceVersion, name='1.0'))
50 self.two_zero_request = FakeRequest(version='2.0')
51 alsoProvides(
52 self.two_zero_request, getUtility(IWebServiceVersion, name='2.0'))
51 self.product = Product()53 self.product = Product()
52 self.project = Project()54 self.project = Project()
5355
@@ -59,7 +61,7 @@
59 # access .bug_count.61 # access .bug_count.
60 self.product._bug_count = 1062 self.product._bug_count = 10
61 register_test_module('testmod', IProduct, IHasBugs)63 register_test_module('testmod', IProduct, IHasBugs)
62 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)64 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
63 self.assertEqual(adapter.bug_count, 10)65 self.assertEqual(adapter.bug_count, 10)
6466
65 def test_operations(self):67 def test_operations(self):
@@ -68,46 +70,46 @@
68 self.product._bug_count = 1070 self.product._bug_count = 10
69 register_test_module('testmod', IProduct, IHasBugs)71 register_test_module('testmod', IProduct, IHasBugs)
70 adapter = getMultiAdapter(72 adapter = getMultiAdapter(
71 (self.product, self.beta_request),73 (self.product, self.one_zero_request),
72 IResourceGETOperation, name='getBugsCount')74 IResourceGETOperation, name='getBugsCount')
73 self.assertEqual(adapter(), '10')75 self.assertEqual(adapter(), '10')
7476
75 def test_contributing_interface_with_differences_between_versions(self):77 def test_contributing_interface_with_differences_between_versions(self):
76 # In the 'beta' version, IHasBranches.development_branches is exported78 # In the '1.0' version, IHasBranches.development_branches is exported
77 # with its original name whereas for the '1.0' version it's exported79 # with its original name whereas for the '2.0' version it's exported
78 # as 'development_branch_10'.80 # as 'development_branch_20'.
79 self.product._dev_branch = Branch('A product branch')81 self.product._dev_branch = Branch('A product branch')
80 register_test_module('testmod', IProduct, IHasBranches)82 register_test_module('testmod', IProduct, IHasBranches)
81 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)83 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
82 self.assertEqual(adapter.development_branch, self.product._dev_branch)84 self.assertEqual(adapter.development_branch, self.product._dev_branch)
8385
84 adapter = getMultiAdapter(86 adapter = getMultiAdapter(
85 (self.product, self.one_zero_request), IEntry)87 (self.product, self.two_zero_request), IEntry)
86 self.assertEqual(88 self.assertEqual(
87 adapter.development_branch_10, self.product._dev_branch)89 adapter.development_branch_20, self.product._dev_branch)
8890
89 def test_mutator_for_just_one_version(self):91 def test_mutator_for_just_one_version(self):
90 # On the 'beta' version, IHasBranches contributes a read only92 # On the '1.0' version, IHasBranches contributes a read only
91 # development_branch field, but on version '1.0' that field can be93 # development_branch field, but on version '2.0' that field can be
92 # modified as we define a mutator for it.94 # modified as we define a mutator for it.
93 self.product._dev_branch = Branch('A product branch')95 self.product._dev_branch = Branch('A product branch')
94 register_test_module('testmod', IProduct, IHasBranches)96 register_test_module('testmod', IProduct, IHasBranches)
95 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)97 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
96 try:98 try:
97 adapter.development_branch = None99 adapter.development_branch = None
98 except AttributeError:100 except AttributeError:
99 pass101 pass
100 else:102 else:
101 self.fail('IHasBranches.development_branch should be read-only '103 self.fail('IHasBranches.development_branch should be read-only '
102 'on the beta version')104 'on the 1.0 version')
103105
104 adapter = getMultiAdapter(106 adapter = getMultiAdapter(
105 (self.product, self.one_zero_request), IEntry)107 (self.product, self.two_zero_request), IEntry)
106 self.assertEqual(108 self.assertEqual(
107 adapter.development_branch_10, self.product._dev_branch)109 adapter.development_branch_20, self.product._dev_branch)
108 adapter.development_branch_10 = None110 adapter.development_branch_20 = None
109 self.assertEqual(111 self.assertEqual(
110 adapter.development_branch_10, None)112 adapter.development_branch_20, None)
111113
112 def test_contributing_to_multiple_interfaces(self):114 def test_contributing_to_multiple_interfaces(self):
113 # Check that the webservice adapter for both IProduct and IProject115 # Check that the webservice adapter for both IProduct and IProject
@@ -115,10 +117,10 @@
115 self.product._bug_count = 10117 self.product._bug_count = 10
116 self.project._bug_count = 100118 self.project._bug_count = 100
117 register_test_module('testmod', IProduct, IProject, IHasBugs)119 register_test_module('testmod', IProduct, IProject, IHasBugs)
118 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)120 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
119 self.assertEqual(adapter.bug_count, 10)121 self.assertEqual(adapter.bug_count, 10)
120122
121 adapter = getMultiAdapter((self.project, self.beta_request), IEntry)123 adapter = getMultiAdapter((self.project, self.one_zero_request), IEntry)
122 self.assertEqual(adapter.bug_count, 100)124 self.assertEqual(adapter.bug_count, 100)
123125
124 def test_multiple_contributing_interfaces(self):126 def test_multiple_contributing_interfaces(self):
@@ -127,7 +129,7 @@
127 self.product._bug_count = 10129 self.product._bug_count = 10
128 self.product._dev_branch = Branch('A product branch')130 self.product._dev_branch = Branch('A product branch')
129 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)131 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
130 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)132 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
131 self.assertEqual(adapter.bug_count, 10)133 self.assertEqual(adapter.bug_count, 10)
132 self.assertEqual(adapter.development_branch, self.product._dev_branch)134 self.assertEqual(adapter.development_branch, self.product._dev_branch)
133135
@@ -139,7 +141,7 @@
139 class Empty:141 class Empty:
140 implements(IEmpty)142 implements(IEmpty)
141 register_test_module('testmod', IEmpty)143 register_test_module('testmod', IEmpty)
142 entry_resource = EntryResource(Empty(), self.beta_request)144 entry_resource = EntryResource(Empty(), self.one_zero_request)
143 self.assertEquals({}, entry_resource.entry._orig_interfaces)145 self.assertEquals({}, entry_resource.entry._orig_interfaces)
144 self.assertEquals([], entry_resource.redacted_fields)146 self.assertEquals([], entry_resource.redacted_fields)
145147
@@ -148,7 +150,7 @@
148 # interface where the field is defined and adapt the context to that150 # interface where the field is defined and adapt the context to that
149 # interface before accessing that field.151 # interface before accessing that field.
150 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)152 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
151 entry_resource = EntryResource(self.product, self.beta_request)153 entry_resource = EntryResource(self.product, self.one_zero_request)
152 self.assertEquals([], entry_resource.redacted_fields)154 self.assertEquals([], entry_resource.redacted_fields)
153155
154 def test_redacted_fields_with_permission_checker(self):156 def test_redacted_fields_with_permission_checker(self):
@@ -161,7 +163,7 @@
161 secure_product = ProxyFactory(163 secure_product = ProxyFactory(
162 self.product,164 self.product,
163 checker=MultiChecker([(IProduct, 'zope.Public')]))165 checker=MultiChecker([(IProduct, 'zope.Public')]))
164 entry_resource = EntryResource(secure_product, self.beta_request)166 entry_resource = EntryResource(secure_product, self.one_zero_request)
165 self.assertEquals([], entry_resource.redacted_fields)167 self.assertEquals([], entry_resource.redacted_fields)
166 finally:168 finally:
167 endInteraction()169 endInteraction()
@@ -183,7 +185,7 @@
183 register_test_module('testmod', IProduct, IHasBranches)185 register_test_module('testmod', IProduct, IHasBranches)
184 self.assertRaises(186 self.assertRaises(
185 ComponentLookupError,187 ComponentLookupError,
186 getMultiAdapter, (dummy, self.beta_request), IEntry)188 getMultiAdapter, (dummy, self.one_zero_request), IEntry)
187189
188 def test_cannot_contribute_to_non_exported_interface(self):190 def test_cannot_contribute_to_non_exported_interface(self):
189 # A contributing interface can only contribute to exported interfaces.191 # A contributing interface can only contribute to exported interfaces.
@@ -218,7 +220,7 @@
218 # different adapters, its type name is that of the main interface and220 # different adapters, its type name is that of the main interface and
219 # not one of its contributors.221 # not one of its contributors.
220 register_test_module('testmod', IProduct, IHasBugs)222 register_test_module('testmod', IProduct, IHasBugs)
221 adapter = getMultiAdapter((self.product, self.beta_request), IEntry)223 adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
222 self.assertEqual(224 self.assertEqual(
223 'product', EntryAdapterUtility(adapter.__class__).singular_type)225 'product', EntryAdapterUtility(adapter.__class__).singular_type)
224226
@@ -308,13 +310,13 @@
308 not_exported = TextLine(title=u'Not exported')310 not_exported = TextLine(title=u'Not exported')
309 development_branch = exported(311 development_branch = exported(
310 Reference(schema=IBranch, readonly=True),312 Reference(schema=IBranch, readonly=True),
311 ('1.0', dict(exported_as='development_branch_10')),313 ('2.0', dict(exported_as='development_branch_20')),
312 ('beta', dict(exported_as='development_branch')))314 ('1.0', dict(exported_as='development_branch')))
313315
314 @mutator_for(development_branch)316 @mutator_for(development_branch)
315 @export_write_operation()317 @export_write_operation()
316 @operation_parameters(value=TextLine())318 @operation_parameters(value=TextLine())
317 @operation_for_version('1.0')319 @operation_for_version('2.0')
318 def set_dev_branch(value):320 def set_dev_branch(value):
319 pass321 pass
320322
@@ -342,7 +344,135 @@
342 adapts(Interface, IHTTPRequest)344 adapts(Interface, IHTTPRequest)
343345
344346
345class ITestServiceRequestBeta(IWebServiceVersion):347# Classes for TestReqireExplicitVersions
346 pass348
347class ITestServiceRequest10(IWebServiceVersion):349class IFieldExportedWithoutAsOf(Interface):
348 pass350 export_as_webservice_entry()
351
352 field = exported(TextLine(), exported=True)
353
354
355class IFieldExportedToEarliestVersionUsingAsOf(Interface):
356 export_as_webservice_entry()
357
358 field = exported(TextLine(), as_of='1.0')
359
360
361class IFieldExportedToLatestVersionUsingAsOf(Interface):
362 export_as_webservice_entry()
363
364 field = exported(TextLine(), as_of='2.0')
365
366
367class IFieldDefiningAttributesBeforeAsOf(Interface):
368 export_as_webservice_entry()
369
370 field = exported(TextLine(), ('1.0', dict(exported=True)),
371 as_of='2.0')
372
373class IFieldAsOfNonexistentVersion(Interface):
374 export_as_webservice_entry()
375
376 field = exported(TextLine(), as_of='nosuchversion')
377
378
379class IFieldDoubleDefinition(Interface):
380 export_as_webservice_entry()
381
382 field = exported(TextLine(), ('2.0', dict(exported_as='name2')),
383 exported_as='name2', as_of='2.0')
384
385
386class TestRequireExplicitVersions(TestCaseWithWebServiceFixtures):
387 """Test behavior when require_explicit_versions is True."""
388
389 def setUp(self):
390 super(TestRequireExplicitVersions, self).setUp()
391 self.utility = getUtility(IWebServiceConfiguration)
392 self.utility.require_explicit_versions = True
393
394 def _assertRaises(self, exception, func, *args, **kwargs):
395 # Approximate the behavior of testtools assertRaises, which
396 # returns the raised exception. I couldn't get
397 # testtools.TestCase to play nicely with zope's Cleanup class.
398 self.assertRaises(exception, func, *args, **kwargs)
399 try:
400 func(*args, **kwargs)
401 except Exception, e:
402 return e
403
404 def test_field_exported_as_of_earlier_version_is_exported_in_subsequent_versions(self):
405
406 interfaces = generate_entry_interfaces(
407 IFieldExportedToEarliestVersionUsingAsOf, [],
408 *self.utility.active_versions)
409 interface_10 = interfaces[0][1]
410 interface_20 = interfaces[1][1]
411 self.assertEquals(interface_10.names(), ['field'])
412 self.assertEquals(interface_20.names(), ['field'])
413
414 def test_field_exported_as_of_later_version_is_not_exported_in_earlier_versions(self):
415
416 interfaces = generate_entry_interfaces(
417 IFieldExportedToLatestVersionUsingAsOf, [],
418 *self.utility.active_versions)
419 interface_10 = interfaces[0][1]
420 interface_20 = interfaces[1][1]
421 self.assertEquals(interface_10.names(), [])
422 self.assertEquals(interface_20.names(), ['field'])
423
424 def test_field_not_exported_using_as_of_fails(self):
425 exception = self._assertRaises(
426 ValueError, generate_entry_interfaces,
427 IFieldExportedWithoutAsOf, [], *self.utility.active_versions)
428 self.assertEquals(
429 str(exception),
430 ('Field "field" in interface "IFieldExportedWithoutAsOf": '
431 'Field was exported in version 1.0, but not by using as_of. '
432 'The service configuration requires that you use as_of.')
433 )
434
435
436 def test_field_cannot_be_both_exported_and_not_exported(self):
437 # If you use the as_of keyword argument, you can't also set
438 # the exported keyword argument to False.
439 exception = self._assertRaises(
440 ValueError, exported, TextLine(), as_of='1.0', exported=False)
441 self.assertEquals(
442 str(exception),
443 ('as_of=1.0 says to export TextLine, but exported=False '
444 'says not to.'))
445
446 def test_field_exported_as_of_nonexistent_version_fails(self):
447 exception = self._assertRaises(
448 ValueError, generate_entry_interfaces,
449 IFieldAsOfNonexistentVersion, [],
450 *self.utility.active_versions)
451 self.assertEquals(
452 str(exception),
453 ('Field "field" in interface "IFieldAsOfNonexistentVersion": '
454 'Unrecognized version "nosuchversion".'))
455
456 def test_field_exported_with_duplicate_attributes_fails(self):
457 # You can't provide a dictionary of attributes for the
458 # version specified in as_of.
459 exception = self._assertRaises(
460 ValueError, generate_entry_interfaces,
461 IFieldDoubleDefinition, [], *self.utility.active_versions)
462 self.assertEquals(
463 str(exception),
464 ('Field "field" in interface "IFieldDoubleDefinition": '
465 'Duplicate annotations for version "2.0".'))
466
467 def test_field_with_annotations_that_precede_as_of_fails(self):
468 # You can't provide a dictionary of attributes for a version
469 # preceding the version specified in as_of.
470 exception = self._assertRaises(
471 ValueError, generate_entry_interfaces,
472 IFieldDefiningAttributesBeforeAsOf, [],
473 *self.utility.active_versions)
474 self.assertEquals(
475 str(exception),
476 ('Field "field" in interface '
477 '"IFieldDefiningAttributesBeforeAsOf": Version "1.0" defined '
478 'after the later version "2.0".'))

Subscribers

People subscribed via source and target branches