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
1=== modified file 'src/lazr/restful/declarations.py'
2--- src/lazr/restful/declarations.py 2011-03-02 20:08:58 +0000
3+++ src/lazr/restful/declarations.py 2011-03-09 16:02:29 +0000
4@@ -212,17 +212,14 @@
5 The dictionary may contain the key 'exported', which controls
6 whether or not to publish this field at all in the given
7 version, and the key 'exported_as', which controls the name to
8- use when publishing the field. as=None means to use the
9- field's internal name.
10+ use when publishing the field. exported_as=None means to use
11+ the field's internal name.
12+
13+ :param as_of: The name of the earliest version to contain this field.
14
15 :param exported_as: the name under which the field is published in
16- the entry in the earliest version of the web service. By
17- default, the field's internal name is used. This is a simpler
18- alternative to the 'versioned_annotations' parameter, for fields
19- whose names don't change in different versions.
20-
21- :param exported: Whether or not the field should be published in
22- the earliest version of the web service.
23+ the entry the first time it shows up (ie. in the 'as_of'
24+ version). By default, the field's internal name is used.
25
26 :raises TypeError: if called on an object which doesn't provide IField.
27 :returns: The field with an added tagged value.
28@@ -233,24 +230,37 @@
29 if IObject.providedBy(field) and not IReference.providedBy(field):
30 raise TypeError("Object exported; use Reference instead.")
31
32- # The first step is to turn the arguments into a
33- # VersionedDict. We'll start by collecting annotations for the
34- # earliest version, which we refer to as None because we don't
35- # know any of the real version strings yet.
36+ # The first step is to turn the arguments into a VersionedDict
37+ # describing the different ways this field is exposed in different
38+ # versions.
39 annotation_stack = VersionedDict()
40- annotation_stack.push(None)
41+ first_version_name = kwparams.pop('as_of', None)
42+ annotation_stack.push(first_version_name)
43 annotation_stack['type'] = FIELD_TYPE
44
45+ if first_version_name is not None:
46+ # The user explicitly said to start publishing this field in a
47+ # particular version.
48+ annotation_stack['_as_of_was_used'] = True
49+ annotation_stack['exported'] = True
50+
51 annotation_key_for_argument_key = {'exported_as' : 'as',
52 'exported' : 'exported',
53 'readonly' : 'readonly'}
54
55 # If keyword parameters are present, they define the field's
56- # behavior for the first version. Incorporate them into the
57- # VersionedDict.
58+ # behavior for the first exposed version. Incorporate them into
59+ # the VersionedDict.
60 for (key, annotation_key) in annotation_key_for_argument_key.items():
61 if key in kwparams:
62+ if (key == "exported" and kwparams[key] == False
63+ and first_version_name is not None):
64+ raise ValueError(
65+ ("as_of=%s says to export %s, but exported=False "
66+ "says not to.") % (
67+ first_version_name, field.__class__.__name__))
68 annotation_stack[annotation_key] = kwparams.pop(key)
69+
70 # If any keywords are left over, raise an exception.
71 if len(kwparams) > 0:
72 raise TypeError("exported got an unexpected keyword "
73@@ -275,7 +285,7 @@
74 # We track the field's mutator information separately because it's
75 # defined in the named operations, not in the fields. The last
76 # thing we want to do is try to insert a foreign value into an
77- # already create annotation stack.
78+ # already created annotation stack.
79 field.setTaggedValue(LAZR_WEBSERVICE_MUTATORS, {})
80
81 return field
82@@ -1320,27 +1330,37 @@
83 versioned_dict = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
84 mutator_annotations = field.queryTaggedValue(LAZR_WEBSERVICE_MUTATORS)
85
86- # If the first version is None and the second version is the
87- # earliest version, consolidate the two versions.
88 earliest_version = versions[0]
89 stack = versioned_dict.stack
90
91- if (len(stack) >= 2
92- and stack[0][0] is None and stack[1][0] == earliest_version):
93-
94- # Make sure the implicit definition of the earliest version
95- # doesn't conflict with the explicit definition. The only
96- # exception is the 'as' value, which is implicitly defined
97- # by the system, not explicitly by the user.
98- for key, value in stack[1][1].items():
99- implicit_value = stack[0][1].get(key)
100- if (implicit_value != value and
101- not (key == 'as' and implicit_value == field.__name__)):
102- raise ValueError(
103- error_prefix + 'Annotation "%s" has conflicting values '
104- 'for the earliest version: "%s" (from keyword arguments) '
105- 'and "%s" (defined explicitly).' % (
106- key, implicit_value, value))
107+ if (len(stack) >= 2 and stack[0][0] is None
108+ and stack[1][0] == earliest_version):
109+ # The behavior of the earliest version is defined with keyword
110+ # arguments, but the first explicitly-defined version also
111+ # refers to the earliest version. We need to consolidate the
112+ # versions.
113+ implicit_earliest_version = stack[0][1]
114+ explicit_earliest_version = stack[1][1]
115+ for key, value in explicit_earliest_version.items():
116+ if key not in implicit_earliest_version:
117+ # This key was defined for the earliest version using a
118+ # configuration dictionary, but not defined at all
119+ # using keyword arguments. The configuration
120+ # dictionary takes precedence.
121+ continue
122+ implicit_value = implicit_earliest_version[key]
123+ if implicit_value == value:
124+ # The two values are in sync.
125+ continue
126+ if key == 'as' and implicit_value == field.__name__:
127+ # The implicit value was set by the system, not by the
128+ # user. The later value will simply take precedence.
129+ continue
130+ raise ValueError(
131+ error_prefix + 'Annotation "%s" has conflicting values '
132+ 'for the earliest version: "%s" (from keyword arguments) '
133+ 'and "%s" (defined explicitly).' % (
134+ key, implicit_value, value))
135 stack[0][1].update(stack[1][1])
136 stack.remove(stack[1])
137
138@@ -1349,6 +1369,19 @@
139 if stack[0][0] is None:
140 stack[0] = (earliest_version, stack[0][1])
141
142+ # If require_explicit_versions is set, make sure the first version
143+ # to set 'exported' also sets '_as_of_was_used'.
144+ if getUtility(IWebServiceConfiguration).require_explicit_versions:
145+ for version, annotations in stack:
146+ if annotations.get('exported', False):
147+ if not annotations.get('_as_of_was_used', False):
148+ raise ValueError(
149+ error_prefix + (
150+ "Field was exported in version %s, but not"
151+ " by using as_of. The service configuration"
152+ " requires that you use as_of." % version))
153+ break
154+
155 # Make sure there is at most one mutator for the earliest version.
156 # If there is one, move it from the mutator-specific dictionary to
157 # the normal tag stack.
158@@ -1386,8 +1419,8 @@
159 if stack[0][0] == earliest_version:
160 new_stack = [stack[0]]
161 else:
162- # The field is not initially published.
163- new_stack = (earliest_version, dict(published=False))
164+ # The field is not initially exported.
165+ new_stack = [(earliest_version, dict(exported=False))]
166 most_recent_tags = new_stack[0][1]
167 most_recent_mutator_tags = earliest_mutator
168 for version in versions[1:]:
169
170=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
171--- src/lazr/restful/docs/webservice-declarations.txt 2011-02-16 15:41:04 +0000
172+++ src/lazr/restful/docs/webservice-declarations.txt 2011-03-09 16:02:29 +0000
173@@ -847,6 +847,55 @@
174 Generating the webservice
175 =========================
176
177+Setup
178+-----
179+
180+Before we can continue, we must define a web service configuration
181+object. Each web service needs to have one of these registered
182+utilities providing basic information about the web service. This one
183+is just a dummy.
184+
185+ >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
186+ >>> from zope.component import provideUtility
187+ >>> from lazr.restful.interfaces import IWebServiceConfiguration
188+ >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
189+ ... active_versions = ["beta", "1.0", "2.0", "3.0"]
190+ ... last_version_with_mutator_named_operations = "1.0"
191+ ... first_version_with_total_size_link = "2.0"
192+ ... code_revision = "1.0b"
193+ ... default_batch_size = 50
194+ >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
195+
196+We must also set up the ability to create versioned requests. This web
197+service has four versions: 'beta', '1.0', '2.0', and '3.0'. We'll
198+need a marker interface for every version, registered as a utility
199+under the name of the version.
200+
201+Each version interface subclasses the previous version's
202+interface. This lets a request use a resource definition for the
203+previous version if it hasn't changed since then.
204+
205+ >>> from zope.component import getSiteManager
206+ >>> from lazr.restful.interfaces import IWebServiceVersion
207+ >>> class ITestServiceRequestBeta(IWebServiceVersion):
208+ ... pass
209+ >>> class ITestServiceRequest10(ITestServiceRequestBeta):
210+ ... pass
211+ >>> class ITestServiceRequest20(ITestServiceRequest10):
212+ ... pass
213+ >>> class ITestServiceRequest30(ITestServiceRequest20):
214+ ... pass
215+ >>> sm = getSiteManager()
216+ >>> for marker, name in [(ITestServiceRequestBeta, 'beta'),
217+ ... (ITestServiceRequest10, '1.0'),
218+ ... (ITestServiceRequest20, '2.0'),
219+ ... (ITestServiceRequest30, '3.0')]:
220+ ... sm.registerUtility(marker, IWebServiceVersion, name=name)
221+
222+ >>> from lazr.restful.testing.webservice import FakeRequest
223+ >>> request = FakeRequest(version='beta')
224+
225+
226 Entry
227 -----
228
229@@ -968,51 +1017,6 @@
230 ... self.base_price = base_price
231 ... self.inventory_number = inventory_number
232
233-Before we can continue, we must define a web service configuration
234-object. Each web service needs to have one of these registered
235-utilities providing basic information about the web service. This one
236-is just a dummy.
237-
238- >>> from lazr.restful.testing.helpers import TestWebServiceConfiguration
239- >>> from zope.component import provideUtility
240- >>> from lazr.restful.interfaces import IWebServiceConfiguration
241- >>> class MyWebServiceConfiguration(TestWebServiceConfiguration):
242- ... active_versions = ["beta", "1.0", "2.0", "3.0"]
243- ... last_version_with_mutator_named_operations = "1.0"
244- ... first_version_with_total_size_link = "2.0"
245- ... code_revision = "1.0b"
246- ... default_batch_size = 50
247- >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
248-
249-We must also set up the ability to create versioned requests. This web
250-service has four versions: 'beta', '1.0', '2.0', and '3.0'. We'll
251-need a marker interface for every version, registered as a utility
252-under the name of the version.
253-
254-Each version interface subclasses the previous version's
255-interface. This lets a request use a resource definition for the
256-previous version if it hasn't changed since then.
257-
258- >>> from zope.component import getSiteManager
259- >>> from lazr.restful.interfaces import IWebServiceVersion
260- >>> class ITestServiceRequestBeta(IWebServiceVersion):
261- ... pass
262- >>> class ITestServiceRequest10(ITestServiceRequestBeta):
263- ... pass
264- >>> class ITestServiceRequest20(ITestServiceRequest10):
265- ... pass
266- >>> class ITestServiceRequest30(ITestServiceRequest20):
267- ... pass
268- >>> sm = getSiteManager()
269- >>> for marker, name in [(ITestServiceRequestBeta, 'beta'),
270- ... (ITestServiceRequest10, '1.0'),
271- ... (ITestServiceRequest20, '2.0'),
272- ... (ITestServiceRequest30, '3.0')]:
273- ... sm.registerUtility(marker, IWebServiceVersion, name=name)
274-
275- >>> from lazr.restful.testing.webservice import FakeRequest
276- >>> request = FakeRequest(version='beta')
277-
278 Now we can turn a Book object into something that implements
279 IBookEntry.
280
281@@ -1047,6 +1051,7 @@
282 ...
283 TypeError: 'IBookSet' isn't exported as an entry.
284
285+
286 Collection
287 ----------
288
289
290=== modified file 'src/lazr/restful/interfaces/_rest.py'
291--- src/lazr/restful/interfaces/_rest.py 2011-03-02 18:49:23 +0000
292+++ src/lazr/restful/interfaces/_rest.py 2011-03-09 16:02:29 +0000
293@@ -514,6 +514,13 @@
294 default = {}
295 )
296
297+ require_explicit_versions = Bool(
298+ default=False,
299+ description=u"""If true, each exported field and named
300+ operation must explicitly declare the first version in which
301+ it appears. If false, fields and operations are published in
302+ all versions.""")
303+
304 last_version_with_mutator_named_operations = TextLine(
305 default=None,
306 description=u"""In earlier versions of lazr.restful, mutator methods
307
308=== modified file 'src/lazr/restful/testing/helpers.py'
309--- src/lazr/restful/testing/helpers.py 2011-02-17 14:56:25 +0000
310+++ src/lazr/restful/testing/helpers.py 2011-03-09 16:02:29 +0000
311@@ -70,6 +70,7 @@
312 code_revision = "1.0b"
313 default_batch_size = 50
314 hostname = 'example.com'
315+ require_explicit_versions = False
316
317 def get_request_user(self):
318 return 'A user'
319
320=== modified file 'src/lazr/restful/testing/webservice.py'
321--- src/lazr/restful/testing/webservice.py 2011-02-17 14:56:25 +0000
322+++ src/lazr/restful/testing/webservice.py 2011-03-09 16:02:29 +0000
323@@ -28,8 +28,9 @@
324 import simplejson
325 import sys
326 from types import ModuleType
327+import unittest
328 import urllib
329-import unittest
330+
331 from urlparse import urljoin
332 import wsgi_intercept
333
334@@ -448,15 +449,15 @@
335 """A marker interface for requests to the '2.0' web service."""
336
337
338-class WebServiceTestCase(CleanUp, unittest.TestCase):
339- """A test case for web service operations."""
340+class TestCaseWithWebServiceFixtures(CleanUp, unittest.TestCase):
341+ """A test case that needs to have some aspects of a web service set up.
342
343- testmodule_objects = []
344+ If the test case is testing a real web service, use
345+ WebServiceTestCase instead.
346+ """
347
348 def setUp(self):
349- """Set the component registry with the given model."""
350- super(WebServiceTestCase, self).setUp()
351-
352+ super(TestCaseWithWebServiceFixtures, self).setUp()
353 # Register a simple configuration object.
354 webservice_configuration = WebServiceTestConfiguration()
355 sm = getGlobalSiteManager()
356@@ -471,7 +472,18 @@
357 sm.registerUtility(
358 IWebServiceTestRequest20, IWebServiceVersion, name='2.0')
359
360+
361+class WebServiceTestCase(TestCaseWithWebServiceFixtures):
362+ """A test case for web service operations."""
363+
364+ testmodule_objects = []
365+
366+ def setUp(self):
367+ """Set the component registry with the given model."""
368+ super(WebServiceTestCase, self).setUp()
369+
370 # Register a service root resource
371+ sm = getGlobalSiteManager()
372 service_root = ServiceRootResource()
373 sm.registerUtility(service_root, IServiceRootResource)
374
375
376=== modified file 'src/lazr/restful/tests/test_declarations.py'
377--- src/lazr/restful/tests/test_declarations.py 2011-01-21 21:08:43 +0000
378+++ src/lazr/restful/tests/test_declarations.py 2011-03-09 16:02:29 +0000
379@@ -1,5 +1,3 @@
380-import unittest
381-
382 from zope.component import (
383 adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)
384 from zope.component.interfaces import ComponentLookupError
385@@ -10,9 +8,15 @@
386 from zope.security.management import endInteraction, newInteraction
387
388 from lazr.restful.declarations import (
389- export_as_webservice_entry, exported, export_read_operation,
390- export_write_operation, mutator_for, operation_for_version,
391- operation_parameters)
392+ export_as_webservice_entry,
393+ exported,
394+ export_read_operation,
395+ export_write_operation,
396+ generate_entry_interfaces,
397+ mutator_for,
398+ operation_for_version,
399+ operation_parameters,
400+ )
401 from lazr.restful.fields import Reference
402 from lazr.restful.interfaces import (
403 IEntry, IResourceGETOperation, IWebServiceConfiguration,
404@@ -22,32 +26,30 @@
405 AttemptToContributeToNonExportedInterface,
406 ConflictInContributingInterfaces, find_interfaces_and_contributors)
407 from lazr.restful._resource import EntryAdapterUtility, EntryResource
408-from lazr.restful.testing.webservice import FakeRequest
409+from lazr.restful.testing.webservice import (
410+ FakeRequest,
411+ TestCaseWithWebServiceFixtures,
412+ )
413 from lazr.restful.testing.helpers import (
414 create_test_module, TestWebServiceConfiguration, register_test_module)
415
416
417-class ContributingInterfacesTestCase(unittest.TestCase):
418+class ContributingInterfacesTestCase(TestCaseWithWebServiceFixtures):
419 """Tests for interfaces that contribute fields/operations to others."""
420
421 def setUp(self):
422- provideUtility(
423- TestWebServiceConfiguration(), IWebServiceConfiguration)
424+ super(ContributingInterfacesTestCase, self).setUp()
425 sm = getSiteManager()
426- sm.registerUtility(
427- ITestServiceRequestBeta, IWebServiceVersion, name='beta')
428- sm.registerUtility(
429- ITestServiceRequest10, IWebServiceVersion, name='1.0')
430 sm.registerAdapter(ProductToHasBugsAdapter)
431 sm.registerAdapter(ProjectToHasBugsAdapter)
432 sm.registerAdapter(ProductToHasBranchesAdapter)
433 sm.registerAdapter(DummyFieldMarshaller)
434- self.beta_request = FakeRequest(version='beta')
435- alsoProvides(
436- self.beta_request, getUtility(IWebServiceVersion, name='beta'))
437 self.one_zero_request = FakeRequest(version='1.0')
438 alsoProvides(
439 self.one_zero_request, getUtility(IWebServiceVersion, name='1.0'))
440+ self.two_zero_request = FakeRequest(version='2.0')
441+ alsoProvides(
442+ self.two_zero_request, getUtility(IWebServiceVersion, name='2.0'))
443 self.product = Product()
444 self.project = Project()
445
446@@ -59,7 +61,7 @@
447 # access .bug_count.
448 self.product._bug_count = 10
449 register_test_module('testmod', IProduct, IHasBugs)
450- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
451+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
452 self.assertEqual(adapter.bug_count, 10)
453
454 def test_operations(self):
455@@ -68,46 +70,46 @@
456 self.product._bug_count = 10
457 register_test_module('testmod', IProduct, IHasBugs)
458 adapter = getMultiAdapter(
459- (self.product, self.beta_request),
460+ (self.product, self.one_zero_request),
461 IResourceGETOperation, name='getBugsCount')
462 self.assertEqual(adapter(), '10')
463
464 def test_contributing_interface_with_differences_between_versions(self):
465- # In the 'beta' version, IHasBranches.development_branches is exported
466- # with its original name whereas for the '1.0' version it's exported
467- # as 'development_branch_10'.
468+ # In the '1.0' version, IHasBranches.development_branches is exported
469+ # with its original name whereas for the '2.0' version it's exported
470+ # as 'development_branch_20'.
471 self.product._dev_branch = Branch('A product branch')
472 register_test_module('testmod', IProduct, IHasBranches)
473- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
474+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
475 self.assertEqual(adapter.development_branch, self.product._dev_branch)
476
477 adapter = getMultiAdapter(
478- (self.product, self.one_zero_request), IEntry)
479+ (self.product, self.two_zero_request), IEntry)
480 self.assertEqual(
481- adapter.development_branch_10, self.product._dev_branch)
482+ adapter.development_branch_20, self.product._dev_branch)
483
484 def test_mutator_for_just_one_version(self):
485- # On the 'beta' version, IHasBranches contributes a read only
486- # development_branch field, but on version '1.0' that field can be
487+ # On the '1.0' version, IHasBranches contributes a read only
488+ # development_branch field, but on version '2.0' that field can be
489 # modified as we define a mutator for it.
490 self.product._dev_branch = Branch('A product branch')
491 register_test_module('testmod', IProduct, IHasBranches)
492- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
493+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
494 try:
495 adapter.development_branch = None
496 except AttributeError:
497 pass
498 else:
499 self.fail('IHasBranches.development_branch should be read-only '
500- 'on the beta version')
501+ 'on the 1.0 version')
502
503 adapter = getMultiAdapter(
504- (self.product, self.one_zero_request), IEntry)
505- self.assertEqual(
506- adapter.development_branch_10, self.product._dev_branch)
507- adapter.development_branch_10 = None
508- self.assertEqual(
509- adapter.development_branch_10, None)
510+ (self.product, self.two_zero_request), IEntry)
511+ self.assertEqual(
512+ adapter.development_branch_20, self.product._dev_branch)
513+ adapter.development_branch_20 = None
514+ self.assertEqual(
515+ adapter.development_branch_20, None)
516
517 def test_contributing_to_multiple_interfaces(self):
518 # Check that the webservice adapter for both IProduct and IProject
519@@ -115,10 +117,10 @@
520 self.product._bug_count = 10
521 self.project._bug_count = 100
522 register_test_module('testmod', IProduct, IProject, IHasBugs)
523- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
524+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
525 self.assertEqual(adapter.bug_count, 10)
526
527- adapter = getMultiAdapter((self.project, self.beta_request), IEntry)
528+ adapter = getMultiAdapter((self.project, self.one_zero_request), IEntry)
529 self.assertEqual(adapter.bug_count, 100)
530
531 def test_multiple_contributing_interfaces(self):
532@@ -127,7 +129,7 @@
533 self.product._bug_count = 10
534 self.product._dev_branch = Branch('A product branch')
535 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
536- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
537+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
538 self.assertEqual(adapter.bug_count, 10)
539 self.assertEqual(adapter.development_branch, self.product._dev_branch)
540
541@@ -139,7 +141,7 @@
542 class Empty:
543 implements(IEmpty)
544 register_test_module('testmod', IEmpty)
545- entry_resource = EntryResource(Empty(), self.beta_request)
546+ entry_resource = EntryResource(Empty(), self.one_zero_request)
547 self.assertEquals({}, entry_resource.entry._orig_interfaces)
548 self.assertEquals([], entry_resource.redacted_fields)
549
550@@ -148,7 +150,7 @@
551 # interface where the field is defined and adapt the context to that
552 # interface before accessing that field.
553 register_test_module('testmod', IProduct, IHasBugs, IHasBranches)
554- entry_resource = EntryResource(self.product, self.beta_request)
555+ entry_resource = EntryResource(self.product, self.one_zero_request)
556 self.assertEquals([], entry_resource.redacted_fields)
557
558 def test_redacted_fields_with_permission_checker(self):
559@@ -161,7 +163,7 @@
560 secure_product = ProxyFactory(
561 self.product,
562 checker=MultiChecker([(IProduct, 'zope.Public')]))
563- entry_resource = EntryResource(secure_product, self.beta_request)
564+ entry_resource = EntryResource(secure_product, self.one_zero_request)
565 self.assertEquals([], entry_resource.redacted_fields)
566 finally:
567 endInteraction()
568@@ -183,7 +185,7 @@
569 register_test_module('testmod', IProduct, IHasBranches)
570 self.assertRaises(
571 ComponentLookupError,
572- getMultiAdapter, (dummy, self.beta_request), IEntry)
573+ getMultiAdapter, (dummy, self.one_zero_request), IEntry)
574
575 def test_cannot_contribute_to_non_exported_interface(self):
576 # A contributing interface can only contribute to exported interfaces.
577@@ -218,7 +220,7 @@
578 # different adapters, its type name is that of the main interface and
579 # not one of its contributors.
580 register_test_module('testmod', IProduct, IHasBugs)
581- adapter = getMultiAdapter((self.product, self.beta_request), IEntry)
582+ adapter = getMultiAdapter((self.product, self.one_zero_request), IEntry)
583 self.assertEqual(
584 'product', EntryAdapterUtility(adapter.__class__).singular_type)
585
586@@ -308,13 +310,13 @@
587 not_exported = TextLine(title=u'Not exported')
588 development_branch = exported(
589 Reference(schema=IBranch, readonly=True),
590- ('1.0', dict(exported_as='development_branch_10')),
591- ('beta', dict(exported_as='development_branch')))
592+ ('2.0', dict(exported_as='development_branch_20')),
593+ ('1.0', dict(exported_as='development_branch')))
594
595 @mutator_for(development_branch)
596 @export_write_operation()
597 @operation_parameters(value=TextLine())
598- @operation_for_version('1.0')
599+ @operation_for_version('2.0')
600 def set_dev_branch(value):
601 pass
602
603@@ -342,7 +344,135 @@
604 adapts(Interface, IHTTPRequest)
605
606
607-class ITestServiceRequestBeta(IWebServiceVersion):
608- pass
609-class ITestServiceRequest10(IWebServiceVersion):
610- pass
611+# Classes for TestReqireExplicitVersions
612+
613+class IFieldExportedWithoutAsOf(Interface):
614+ export_as_webservice_entry()
615+
616+ field = exported(TextLine(), exported=True)
617+
618+
619+class IFieldExportedToEarliestVersionUsingAsOf(Interface):
620+ export_as_webservice_entry()
621+
622+ field = exported(TextLine(), as_of='1.0')
623+
624+
625+class IFieldExportedToLatestVersionUsingAsOf(Interface):
626+ export_as_webservice_entry()
627+
628+ field = exported(TextLine(), as_of='2.0')
629+
630+
631+class IFieldDefiningAttributesBeforeAsOf(Interface):
632+ export_as_webservice_entry()
633+
634+ field = exported(TextLine(), ('1.0', dict(exported=True)),
635+ as_of='2.0')
636+
637+class IFieldAsOfNonexistentVersion(Interface):
638+ export_as_webservice_entry()
639+
640+ field = exported(TextLine(), as_of='nosuchversion')
641+
642+
643+class IFieldDoubleDefinition(Interface):
644+ export_as_webservice_entry()
645+
646+ field = exported(TextLine(), ('2.0', dict(exported_as='name2')),
647+ exported_as='name2', as_of='2.0')
648+
649+
650+class TestRequireExplicitVersions(TestCaseWithWebServiceFixtures):
651+ """Test behavior when require_explicit_versions is True."""
652+
653+ def setUp(self):
654+ super(TestRequireExplicitVersions, self).setUp()
655+ self.utility = getUtility(IWebServiceConfiguration)
656+ self.utility.require_explicit_versions = True
657+
658+ def _assertRaises(self, exception, func, *args, **kwargs):
659+ # Approximate the behavior of testtools assertRaises, which
660+ # returns the raised exception. I couldn't get
661+ # testtools.TestCase to play nicely with zope's Cleanup class.
662+ self.assertRaises(exception, func, *args, **kwargs)
663+ try:
664+ func(*args, **kwargs)
665+ except Exception, e:
666+ return e
667+
668+ def test_field_exported_as_of_earlier_version_is_exported_in_subsequent_versions(self):
669+
670+ interfaces = generate_entry_interfaces(
671+ IFieldExportedToEarliestVersionUsingAsOf, [],
672+ *self.utility.active_versions)
673+ interface_10 = interfaces[0][1]
674+ interface_20 = interfaces[1][1]
675+ self.assertEquals(interface_10.names(), ['field'])
676+ self.assertEquals(interface_20.names(), ['field'])
677+
678+ def test_field_exported_as_of_later_version_is_not_exported_in_earlier_versions(self):
679+
680+ interfaces = generate_entry_interfaces(
681+ IFieldExportedToLatestVersionUsingAsOf, [],
682+ *self.utility.active_versions)
683+ interface_10 = interfaces[0][1]
684+ interface_20 = interfaces[1][1]
685+ self.assertEquals(interface_10.names(), [])
686+ self.assertEquals(interface_20.names(), ['field'])
687+
688+ def test_field_not_exported_using_as_of_fails(self):
689+ exception = self._assertRaises(
690+ ValueError, generate_entry_interfaces,
691+ IFieldExportedWithoutAsOf, [], *self.utility.active_versions)
692+ self.assertEquals(
693+ str(exception),
694+ ('Field "field" in interface "IFieldExportedWithoutAsOf": '
695+ 'Field was exported in version 1.0, but not by using as_of. '
696+ 'The service configuration requires that you use as_of.')
697+ )
698+
699+
700+ def test_field_cannot_be_both_exported_and_not_exported(self):
701+ # If you use the as_of keyword argument, you can't also set
702+ # the exported keyword argument to False.
703+ exception = self._assertRaises(
704+ ValueError, exported, TextLine(), as_of='1.0', exported=False)
705+ self.assertEquals(
706+ str(exception),
707+ ('as_of=1.0 says to export TextLine, but exported=False '
708+ 'says not to.'))
709+
710+ def test_field_exported_as_of_nonexistent_version_fails(self):
711+ exception = self._assertRaises(
712+ ValueError, generate_entry_interfaces,
713+ IFieldAsOfNonexistentVersion, [],
714+ *self.utility.active_versions)
715+ self.assertEquals(
716+ str(exception),
717+ ('Field "field" in interface "IFieldAsOfNonexistentVersion": '
718+ 'Unrecognized version "nosuchversion".'))
719+
720+ def test_field_exported_with_duplicate_attributes_fails(self):
721+ # You can't provide a dictionary of attributes for the
722+ # version specified in as_of.
723+ exception = self._assertRaises(
724+ ValueError, generate_entry_interfaces,
725+ IFieldDoubleDefinition, [], *self.utility.active_versions)
726+ self.assertEquals(
727+ str(exception),
728+ ('Field "field" in interface "IFieldDoubleDefinition": '
729+ 'Duplicate annotations for version "2.0".'))
730+
731+ def test_field_with_annotations_that_precede_as_of_fails(self):
732+ # You can't provide a dictionary of attributes for a version
733+ # preceding the version specified in as_of.
734+ exception = self._assertRaises(
735+ ValueError, generate_entry_interfaces,
736+ IFieldDefiningAttributesBeforeAsOf, [],
737+ *self.utility.active_versions)
738+ self.assertEquals(
739+ str(exception),
740+ ('Field "field" in interface '
741+ '"IFieldDefiningAttributesBeforeAsOf": Version "1.0" defined '
742+ 'after the later version "2.0".'))

Subscribers

People subscribed via source and target branches