Merge lp:~leonardr/lazr.restful/entry-introduced-in-version into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: 183
Proposed branch: lp:~leonardr/lazr.restful/entry-introduced-in-version
Merge into: lp:lazr.restful
Diff against target: 1249 lines (+606/-151)
6 files modified
src/lazr/restful/declarations.py (+153/-88)
src/lazr/restful/docs/utils.txt (+27/-7)
src/lazr/restful/docs/webservice-declarations.txt (+31/-20)
src/lazr/restful/tests/test_declarations.py (+190/-26)
src/lazr/restful/tests/test_utils.py (+111/-1)
src/lazr/restful/utils.py (+94/-9)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/entry-introduced-in-version
Reviewer Review Type Date Requested Status
Ian Booth (community) *code Approve
Tim Penhey (community) code Needs Fixing
Review via email: mp+53704@code.launchpad.net

Description of the change

For the first time ever, this branch makes it possible to publish an entry only in certain versions of the web service. Previously when you called export_as_webservice_entry(), the entry was created (in slightly varying forms) for every single version. Now you can make IFoo show up starting in '1.0' instead of 'beta', or have IBar show up in 'beta' but not in '1.0'. You can also give the entry different names in different versions (to fix a typo, or because the terminology changed). So IFoo might be called "foo/foos" in beta, and "foo/fooes" in 1.0.

To see how this works, look at the test classes defined in test_declarations.py. Here are some simpler ones:

class INotInitiallyExported(Interface):
    # An entry that's not exported in the first version of the web
    # service.
    export_as_webservice_entry(as_of="2.0")

class INotPresentInLaterVersion(Interface):
    # An entry that's only exported in the first version of the web
    # service.
    export_as_webservice_entry(
        versioned_annotations=[('2.0', dict(exported=False))])

This work is unfinished, but what I have doesn't break anything, and it's almost exactly 800 lines, so let's review it. Notable unfinished bits:

1. I have not tested the multiversion code in conjunction with contributes_to, and I don't know whether it works.

2. A Reference field can be a link to an entry that isn't even exposed in the current version. The return value of a named operation can also be an entry that isn't exposed in the current version. I need to add checks for this when the web service is being built, or it will cause huge problems at runtime. (This is why I didn't implement the entry code this way in the first place.)

Features I considered but rejected:

1. You can say that IFoo.myfield is published in 2.0, even if IFoo itself is not published in 2.0. Enforcing this would stop some mistakes, but it would require you to un-publish every one of IFoo's fields as a prerequisite for un-publishing IFoo itself.

How it works:

export_as_webservice_entry now takes an 'as_of' argument, just like the exported() function used on fields. If require_explicit_versions is True, then as_of is *required*, just like it is for fields.

The tags describing a webservice entry across versions (eg. the plural name is "foos" in beta and "fooes" in 1.0) are now stored in a VersionedDict, just like they are for fields. Once the tags are gathered, the VersionDict is normalized, so that it includes a dictionary for every single web service version. Then I iterate over the versions as before, creating an entry interface for each version *except* the ones for which this entry is not published.

Figuring out the complete definition of an entry is now the same kind of thing as figuring out the definition of one of its fields. As such, I've refactored some code and started using it for both fields and entries. Notably, the helper function _enforce_explicit_version(), which makes sure you specified as_of if it's required, and the method VersionedDict.normalize_for_versions(), which makes sure the user didn't define versions out of order, and "fills in the blanks" so that the dictionary includes a set of annotations for every single version.

I had a bad cold while I wrote this, but I think it's not too crazy.

To post a comment you must log in.
Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Leonard,

This looks good to me. I have some small fixme's:

diff line 36: param should be "versioned_annotations" not "annotations"
diff line 122: you define stack = entry_tags.stack presumably to make less typing below but then forget to use stack
Perhaps you could tidy up the zope.security.management imports as a drive by in test_utils? The ones in test_declarations also need some lovin' if you are so inclined - there's unused ones as well as formatting issues.

A concern/question:

>> def normalize_for_versions(self, versions, default_dictionary={}, error_prefix=''):

The default_dictionary argument defaults to {} but this is mutable. This can lead to unintended side effects since default args are only evaluated once when the function is defined. It doesn't look like anything bad happens in this case but my preference is to use default_dictionary=dict() just to be safe and explicitly correct in all cases.

A question:

In normalize_for_versions(self, versions, ...), do the later, more recent versions appear towards the front of the versions list? Perhaps the docstring could mention this just to clarify since being unfamiliar with the implementation, it was unclear to me without trying to figure it out.

A suggestion:

In places, instead of using 2-tuple's, eg versioned_annotations parameter to export_as_webservice_entry(), you could consider using a namedtuple. This will make code like:

    if entry_tags.stack[0][0] is None:
        entry_tags.stack[0] = (earliest_version, entry_tags.stack[0][1])

much easier to read, especially for people not previously 100% familiar with the code. It would become something like:

    if entry_tags.stack[0].version is None:
        entry_tags.stack[0] = (earliest_version, entry_tags.stack[0].annotations)

Also the return value of generate_entry_interfaces. So

interfaces = generate_entry_interfaces(...)
... interfaces[0][0]
... interfaces[0][1]

becomes

interfaces = generate_entry_interfaces(...)
... interfaces[0].version
... interfaces[0].interface

review: Needs Fixing
Revision history for this message
Tim Penhey (thumper) wrote :

Does lazr.restful use testtools for the TestCase?

If it does, assertRaises returns the exception, so
test_non_existent_annotation_fails could be somewhat
simplified.

It seems that normalize_for_versions is only passed
no dict through in tests. I believe the more used
approach is to have the default value to be None, and
have something like:

  if default_dictionary is None:
    default_dictionary = {}

I wish we had a way to have contracts on the params.
In C++ it would be a "const" dict. Here though we are
doing a deep copy. My preference, even though you don't
mutate the dict now, would be to use None as the param.

review: Needs Fixing (code)
Revision history for this message
Leonard Richardson (leonardr) wrote :

> Does lazr.restful use testtools for the TestCase?
>
> If it does, assertRaises returns the exception, so
> test_non_existent_annotation_fails could be somewhat
> simplified.

I tried this; the problem is that the exception is raised *during a class definition*. I decided that the try/catch was more readable than calling assertRaises on the 'type' function.

198. By Leonard Richardson

Cleaned up imports.

199. By Leonard Richardson

Don't set a mutable defalt for default_dictionary.

200. By Leonard Richardson

VersionedDict is now a stack of namedtuples, for easier comprehension.

201. By Leonard Richardson

Use the namedtuple for everything, and hide the details of namedtuples from declarations.py by giving it rename_version to use instead.

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

Here's my incremental diff:

http://pastebin.ubuntu.com/581586/

The namedtuple idea is a good one. It forced me to improve the encapsulation of VersionedDict by writing the rename_version() method. I'm a little worried about code that violates encapsulation by sticking non-namedtuples into a VersionedDict, since this branch will cause such code to break. But lazr.restful itself no longer contains any such code, so I'm not _too_ worried about it.

202. By Leonard Richardson

generate_entry_interfaces and generate_entry_adapters now return VersionedObject namedtuples.

203. By Leonard Richardson

Consistently use VersionedObject instead of VersionedDict.

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

Here's a better diff. I use one VersionedObject namedtuple class for all purposes: interfaces, adapters, and annotation dictionaries.

http://pastebin.ubuntu.com/581602/

Revision history for this message
Ian Booth (wallyworld) wrote :

Thanks for incorporating the suggested changes. The use of namedtuple does make the code more readable.

review: Approve (*code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/declarations.py'
2--- src/lazr/restful/declarations.py 2011-03-10 13:53:18 +0000
3+++ src/lazr/restful/declarations.py 2011-03-17 14:24:12 +0000
4@@ -57,7 +57,10 @@
5
6 from lazr.delegates import Passthrough
7
8-from lazr.restful.fields import CollectionField, Reference
9+from lazr.restful.fields import (
10+ CollectionField,
11+ Reference,
12+ )
13 from lazr.restful.interface import copy_field
14 from lazr.restful.interfaces import (
15 ICollection,
16@@ -72,11 +75,21 @@
17 LAZR_WEBSERVICE_NS,
18 )
19 from lazr.restful import (
20- Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink)
21+ Collection,
22+ Entry,
23+ EntryAdapterUtility,
24+ ResourceOperation,
25+ ObjectLink,
26+ )
27 from lazr.restful.security import protect_schema
28 from lazr.restful.utils import (
29- camelcase_to_underscore_separated, get_current_web_service_request,
30- make_identifier_safe, VersionedDict, is_total_size_link_active)
31+ camelcase_to_underscore_separated,
32+ get_current_web_service_request,
33+ make_identifier_safe,
34+ VersionedDict,
35+ VersionedObject,
36+ is_total_size_link_active,
37+ )
38
39 LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS
40 LAZR_WEBSERVICE_MUTATORS = '%s.exported.mutators' % LAZR_WEBSERVICE_NS
41@@ -88,6 +101,16 @@
42 'destructor', 'factory', 'read_operation', 'write_operation',
43 REMOVED_OPERATION_TYPE)
44
45+# These are the only valid keys to be found in an entry's
46+# version-specific annotation dictionary.
47+ENTRY_ANNOTATION_KEYS = set([
48+ 'contributes_to',
49+ 'exported',
50+ 'plural_name',
51+ 'publish_web_link',
52+ 'singular_name',
53+ ])
54+
55
56 class REQUEST_USER:
57 """Marker class standing in for the user of the current request.
58@@ -130,7 +153,8 @@
59
60
61 def export_as_webservice_entry(singular_name=None, plural_name=None,
62- contributes_to=None, publish_web_link=True):
63+ contributes_to=None, publish_web_link=True,
64+ as_of=None, versioned_annotations=None):
65 """Mark the content interface as exported on the web service as an entry.
66
67 If contributes_to is a non-empty sequence of Interfaces, this entry will
68@@ -149,11 +173,24 @@
69 set to True, the representation of this entry will include a
70 web_link pointing to the corresponding page on the website. If
71 False, web_link will be omitted.
72+ :param as_of: The first version of the web service to feature this entry.
73+ :param versioned_annotations: A list of 2-tuples (version,
74+ {params}), with more recent web service versions earlier in
75+ the list and older versions later in the list.
76+
77+ A 'params' dictionary may contain the key 'exported', which
78+ controls whether or not to publish the entry at all in the
79+ given version. It may also contain the keys 'singular_name',
80+ 'plural_name', 'contributes_to', or 'publish_web_link', which
81+ work just like the corresponding arguments to this method.
82 """
83 _check_called_from_interface_def('export_as_webservice_entry()')
84 def mark_entry(interface):
85 """Class advisor that tags the interface once it is created."""
86 _check_interface('export_as_webservice_entry()', interface)
87+
88+ annotation_stack = VersionedDict()
89+
90 if singular_name is None:
91 # By convention, interfaces are called IWord1[Word2...]. The
92 # default behavior assumes this convention and yields a
93@@ -162,17 +199,37 @@
94 interface.__name__[1:])
95 else:
96 my_singular_name = singular_name
97- if plural_name is None:
98- # Apply default pluralization rule.
99- my_plural_name = my_singular_name + 's'
100- else:
101- my_plural_name = plural_name
102-
103- interface.setTaggedValue(
104- LAZR_WEBSERVICE_EXPORTED, dict(
105- type=ENTRY_TYPE, singular_name=my_singular_name,
106- plural_name=my_plural_name, contributes_to=contributes_to,
107- publish_web_link=publish_web_link))
108+
109+ # Turn the named arguments into a dictionary for the first
110+ # exported version.
111+ initial_version = dict(
112+ type=ENTRY_TYPE, singular_name=my_singular_name,
113+ plural_name=plural_name, contributes_to=contributes_to,
114+ publish_web_link=publish_web_link, exported=True,
115+ _as_of_was_used=(not as_of is None))
116+
117+ afterwards = versioned_annotations or []
118+ for version, annotations in itertools.chain(
119+ [(as_of, initial_version)], afterwards):
120+ annotation_stack.push(version)
121+
122+ for key, value in annotations.items():
123+ if annotations != initial_version:
124+ # Make sure that the 'annotations' dict
125+ # contains only recognized annotations.
126+ if key not in ENTRY_ANNOTATION_KEYS:
127+ raise ValueError(
128+ 'Unrecognized annotation for version "%s": '
129+ '"%s"' % (version, key))
130+ annotation_stack[key] = value
131+ # If this version provides a singular name but not a
132+ # plural name, apply the default pluralization rule.
133+ if (annotations.get('singular_name') is not None
134+ and annotations.get('plural_name') is None):
135+ annotation_stack['plural_name'] = (
136+ annotations['singular_name'] + 's')
137+
138+ interface.setTaggedValue(LAZR_WEBSERVICE_EXPORTED, annotation_stack)
139
140 # Set the name of the fields that didn't specify it using the
141 # 'export_as' parameter in exported(). This must be done here,
142@@ -908,8 +965,7 @@
143 _check_tagged_interface(interface, 'entry')
144 versions = list(versions)
145
146- # First off, make sure any given version defines only one
147- # destructor method.
148+ # Make sure any given version defines only one destructor method.
149 destructor_for_version = {}
150 for name, method in interface.namesAndDescriptions(True):
151 if not IMethod.providedBy(method):
152@@ -929,10 +985,33 @@
153 destructor.__name__))
154 destructor_for_version[version] = method
155
156+ # Build a data set describing this entry, as it appears in each
157+ # version of the web service in which it appears at all.
158+ entry_tags = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
159+ stack = entry_tags.stack
160+
161+ earliest_version = versions[0]
162+ # Now that we know the name of the earliest version, get rid of
163+ # any None at the beginning of the stack.
164+ if stack[0].version is None:
165+ entry_tags.rename_version(None, earliest_version)
166+
167+ # If require_explicit_versions is set, make sure the first version
168+ # to set 'exported' also sets '_as_of_was_used'.
169+ _enforce_explicit_version(
170+ entry_tags, 'Entry "%s": ' % interface.__name__)
171+
172+ # Make sure there's one set of entry tags for every version of the
173+ # web service, including versions in which this entry is not
174+ # published.
175+ entry_tags.normalize_for_versions(
176+ versions, {'type': ENTRY_TYPE, 'exported': False},
177+ 'Interface "%s": ' % interface.__name__)
178+
179 # Next, we'll normalize each published field. A normalized field
180- # has a set of annotations for every version. We'll make a list of
181- # the published fields, which we'll iterate over once for each
182- # version.
183+ # has a set of annotations for every version in which the entry is
184+ # published. We'll make a list of the published fields, which
185+ # we'll iterate over once for each version.
186 tags_for_published_fields = []
187 for iface in itertools.chain([interface], contributors):
188 for name, field in getFields(iface).items():
189@@ -943,15 +1022,18 @@
190 error_message_prefix = (
191 'Field "%s" in interface "%s": ' % (name, iface.__name__))
192 _normalize_field_annotations(field, versions, error_message_prefix)
193- tags_for_published_fields.append((name, field, tag_stack.stack))
194+ tags_for_published_fields.append((name, field, tag_stack))
195
196 generated_interfaces = []
197+ entry_tags = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
198 for version in versions:
199+ entry_tags_this_version = entry_tags.dict_for_name(version)
200+ if entry_tags_this_version.get('exported') is False:
201+ # Don't define an entry interface for this version at all.
202+ continue
203 attrs = {}
204 for name, field, tag_stack in tags_for_published_fields:
205- tags = [tags for tag_version, tags in tag_stack
206- if tag_version == version]
207- tags = tags[0]
208+ tags = tag_stack.dict_for_name(version)
209 if tags.get('exported') is False:
210 continue
211 mutated_by, mutated_by_annotations = tags.get(
212@@ -978,12 +1060,12 @@
213 class_name, bases=(IEntry, ), attrs=attrs,
214 __doc__=interface.__doc__, __module__=interface.__module__)
215
216- tag = interface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED)
217+ versioned_tag = entry_tags.dict_for_name(version)
218 entry_interface.setTaggedValue(LAZR_WEBSERVICE_NAME, dict(
219- singular=tag['singular_name'],
220- plural=tag['plural_name'],
221- publish_web_link=tag['publish_web_link']))
222- generated_interfaces.append((version, entry_interface))
223+ singular=versioned_tag['singular_name'],
224+ plural=versioned_tag['plural_name'],
225+ publish_web_link=versioned_tag['publish_web_link']))
226+ generated_interfaces.append(VersionedObject(version, entry_interface))
227 return generated_interfaces
228
229
230@@ -1082,7 +1164,7 @@
231 classImplements(factory, webservice_interface)
232 protect_schema(
233 factory, webservice_interface, write_permission=CheckerPublic)
234- adapters.append((version, factory))
235+ adapters.append(VersionedObject(version, factory))
236 return adapters
237
238
239@@ -1345,14 +1427,14 @@
240 earliest_version = versions[0]
241 stack = versioned_dict.stack
242
243- if (len(stack) >= 2 and stack[0][0] is None
244- and stack[1][0] == earliest_version):
245+ if (len(stack) >= 2 and stack[0].version is None
246+ and stack[1].version == earliest_version):
247 # The behavior of the earliest version is defined with keyword
248 # arguments, but the first explicitly-defined version also
249 # refers to the earliest version. We need to consolidate the
250 # versions.
251- implicit_earliest_version = stack[0][1]
252- explicit_earliest_version = stack[1][1]
253+ implicit_earliest_version = stack[0].object
254+ explicit_earliest_version = stack[1].object
255 for key, value in explicit_earliest_version.items():
256 if key not in implicit_earliest_version:
257 # This key was defined for the earliest version using a
258@@ -1373,26 +1455,17 @@
259 'for the earliest version: "%s" (from keyword arguments) '
260 'and "%s" (defined explicitly).' % (
261 key, implicit_value, value))
262- stack[0][1].update(stack[1][1])
263+ stack[0].object.update(stack[1].object)
264 stack.remove(stack[1])
265
266 # Now that we know the name of the earliest version, get rid of
267 # any None at the beginning of the stack.
268- if stack[0][0] is None:
269- stack[0] = (earliest_version, stack[0][1])
270+ if stack[0].version is None:
271+ versioned_dict.rename_version(None, earliest_version)
272
273 # If require_explicit_versions is set, make sure the first version
274 # to set 'exported' also sets '_as_of_was_used'.
275- if getUtility(IWebServiceConfiguration).require_explicit_versions:
276- for version, annotations in stack:
277- if annotations.get('exported', False):
278- if not annotations.get('_as_of_was_used', False):
279- raise ValueError(
280- error_prefix + (
281- "Field was exported in version %s, but not"
282- " by using as_of. The service configuration"
283- " requires that you use as_of." % version))
284- break
285+ _enforce_explicit_version(versioned_dict, error_prefix)
286
287 # Make sure there is at most one mutator for the earliest version.
288 # If there is one, move it from the mutator-specific dictionary to
289@@ -1406,61 +1479,53 @@
290 "found for earliest version %s." % earliest_version)
291 earliest_mutator = implicit_earliest_mutator or explicit_earliest_mutator
292 if earliest_mutator is not None:
293- stack[0][1]['mutator_annotations'] = earliest_mutator
294-
295- # Do some error checking.
296- max_index = -1
297- for version, tags in versioned_dict.stack:
298- try:
299- version_index = versions.index(version)
300- except ValueError:
301- raise ValueError(
302- error_prefix + 'Unrecognized version "%s".' % version)
303- if version_index == max_index:
304- raise ValueError(
305- error_prefix + 'Duplicate annotations for version '
306- '"%s".' % version)
307- if version_index < max_index:
308- raise ValueError(
309- error_prefix + 'Version "%s" defined after '
310- 'the later version "%s".' % (version, versions[max_index]))
311- max_index = version_index
312+ stack[0].object['mutator_annotations'] = earliest_mutator
313
314 # Fill out the stack so that there is one set of tags for each
315 # version.
316- if stack[0][0] == earliest_version:
317- new_stack = [stack[0]]
318- else:
319- # The field is not initially exported.
320- new_stack = [(earliest_version, dict(exported=False))]
321- most_recent_tags = new_stack[0][1]
322+ versioned_dict.normalize_for_versions(
323+ versions, dict(exported=False), error_prefix)
324+
325+ # Make sure that a mutator defined in version N is inherited in
326+ # version N+1.
327 most_recent_mutator_tags = earliest_mutator
328 for version in versions[1:]:
329 most_recent_mutator_tags = mutator_annotations.get(
330 version, most_recent_mutator_tags)
331- match = [(stack_version, stack_tags)
332- for stack_version, stack_tags in stack
333- if stack_version == version]
334- if len(match) == 0:
335- # This version has no tags of its own. Use a copy of the
336- # most recent tags.
337- new_stack.append((version, copy.deepcopy(most_recent_tags)))
338- else:
339- # This version has tags of its own. Use them unaltered,
340- # and set them up to be used in the future.
341- new_stack.append(match[0])
342- most_recent_tags = match[0][1]
343-
344 # Install a (possibly inherited) mutator for this field in
345 # this version.
346 if most_recent_mutator_tags is not None:
347- new_stack[-1][1]['mutator_annotations'] = copy.deepcopy(
348+ tags_for_version = versioned_dict.dict_for_name(version)
349+ tags_for_version['mutator_annotations'] = copy.deepcopy(
350 most_recent_mutator_tags)
351
352- versioned_dict.stack = new_stack
353 return field
354
355
356+def _enforce_explicit_version(versioned_dict, error_prefix):
357+ """Raise ValueError if the explicit version requirement is not met.
358+
359+ If the configuration has `require_explicit_versions` set, then
360+ the first version in the given VersionedDict to include a True
361+ value for 'exported' must also include a True value for
362+ '_as_of_was_used'.
363+
364+ :param versioned_dict: a VersionedDict.
365+ :param error_prefix: a string to be prepended onto any error message.
366+ """
367+ if not getUtility(IWebServiceConfiguration).require_explicit_versions:
368+ return
369+
370+ for version, annotations in versioned_dict.stack:
371+ if annotations.get('exported', False):
372+ if not annotations.get('_as_of_was_used', False):
373+ raise ValueError(
374+ error_prefix + "Exported in version %s, but not"
375+ " by using as_of. The service configuration"
376+ " requires that you use as_of." % version)
377+ break
378+
379+
380 def _version_name(version):
381 """Return a human-readable version name.
382
383
384=== modified file 'src/lazr/restful/docs/utils.txt'
385--- src/lazr/restful/docs/utils.txt 2010-01-20 15:21:38 +0000
386+++ src/lazr/restful/docs/utils.txt 2011-03-17 14:24:12 +0000
387@@ -65,11 +65,15 @@
388 >>> sorted(stack.items())
389 [('key', 'value')]
390
391-You can pop a named dict off a stack: you'll get back a tuple
392-(name, dict).
393+You can pop a named dict off a stack: you'll get back a named tuple
394+with 'version' and 'object' attributes. The 'object' is the
395+dictionary.
396
397- >>> print stack.pop()
398- ('dict #1', {'key': 'value'})
399+ >>> pair = stack.pop()
400+ >>> print pair.version
401+ dict #1
402+ >>> print pair.object
403+ {'key': 'value'}
404
405 >>> stack.push('dict #1')
406 >>> stack['key'] = 'value'
407@@ -104,6 +108,19 @@
408 >>> sorted(stack.items())
409 [('key', 'Second dict value'), ('key2', {'key': 'value'})]
410
411+You can find the dict for a given name with dict_for_name():
412+
413+ >>> for key, value in sorted(stack.dict_for_name('dict #1').items()):
414+ ... print "%s: %s" % (key, value)
415+ key: value
416+ key2: {'key': 'value'}
417+
418+You can rename a version with rename_version():
419+
420+ >>> stack.rename_version('dict #2', 'Renamed dict')
421+ >>> stack.dict_names
422+ ['dict #1', 'Renamed dict']
423+
424 Suppressing the copy operation
425 ------------------------------
426
427@@ -121,7 +138,7 @@
428 The dictionary that defines 'key' and 'key2' is still there...
429
430 >>> stack.dict_names
431- ['dict #1', 'dict #2', 'An empty dictionary']
432+ ['dict #1', 'Renamed dict', 'An empty dictionary']
433
434 ...but 'key' and 'key2' are no longer accessible.
435
436@@ -141,8 +158,11 @@
437
438 If you pop the formerly empty dictionary off the stack...
439
440- >>> print stack.pop()
441- ('An empty dictionary', {'key': 'Brand new value'})
442+ >>> pair = stack.pop()
443+ >>> print pair.version
444+ An empty dictionary
445+ >>> print pair.object
446+ {'key': 'Brand new value'}
447
448 ...'key' and 'key2' are visible again.
449
450
451=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
452--- src/lazr/restful/docs/webservice-declarations.txt 2011-03-08 19:59:46 +0000
453+++ src/lazr/restful/docs/webservice-declarations.txt 2011-03-17 14:24:12 +0000
454@@ -69,7 +69,9 @@
455 ... "%s: %s" %(key, format_value(value))
456 ... for key, value in sorted(tag.items()))
457 >>> print_export_tag(IBook)
458+ _as_of_was_used: False
459 contributes_to: None
460+ exported: True
461 plural_name: 'books'
462 publish_web_link: True
463 singular_name: 'book'
464@@ -148,6 +150,7 @@
465 ... export_as_webservice_entry(publish_web_link=False)
466 ... field = exported(TextLine(title=u"A field."))
467 >>> print_export_tag(INotOnTheWebsite)
468+ _as_of_was_used: False
469 contributes_to: None
470 ...
471 publish_web_link: False
472@@ -834,7 +837,9 @@
473 into IDeveloper).
474
475 >>> print_export_tag(IDeveloper)
476+ _as_of_was_used: False
477 contributes_to: [<InterfaceClass __builtin__.IUser>]
478+ exported: True
479 plural_name: 'developers'
480 publish_web_link: True
481 singular_name: 'developer'
482@@ -983,9 +988,10 @@
483 Services"). This web service only has one version, so there's only one
484 adapter.
485
486- >>> [(version, entry_adapter_factory)] = entry_adapter_factories
487- >>> print version
488+ >>> [factory] = entry_adapter_factories
489+ >>> print factory.version
490 beta
491+ >>> entry_adapter_factory = factory.object
492
493 The generated adapter provides the webservice interface:
494
495@@ -1435,14 +1441,16 @@
496
497 Generate the entry interface and adapter...
498
499- >>> hastext_entry_interfaces = generate_entry_interfaces(
500+ >>> [hastext_entry_interface] = generate_entry_interfaces(
501 ... IHasText, [], 'beta')
502- >>> [(beta, hastext_entry_interface)] = hastext_entry_interfaces
503- >>> [(beta, hastext_entry_adapter_factory)] = generate_entry_adapters(
504- ... IHasText, [], hastext_entry_interfaces)
505+ >>> [hastext_entry_adapter_factory] = generate_entry_adapters(
506+ ... IHasText, [], [hastext_entry_interface])
507
508 >>> obj = HasText()
509- >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request)
510+ >>> print hastext_entry_adapter_factory.version
511+ beta
512+ >>> hastext_entry_adapter = hastext_entry_adapter_factory.object(
513+ ... obj, request)
514
515 ...and you'll have an object that invokes set_text() when you set the
516 'text' attribute.
517@@ -1456,7 +1464,7 @@
518 The original interface defines 'text' as read-only, but the
519 generated interface does not.
520
521- >>> hastext_entry_interface.get('text').readonly
522+ >>> hastext_entry_interface.object.get('text').readonly
523 False
524
525 It's not necessary to expose the mutator method as a write operation.
526@@ -1944,15 +1952,15 @@
527 >>> foo, bar = generate_entry_interfaces(
528 ... IAmbiguousMultiVersion, [], 'foo', 'bar')
529
530- >>> print foo[0]
531+ >>> print foo.version
532 foo
533- >>> dump_entry_interface(foo[1])
534+ >>> dump_entry_interface(foo.object)
535 field2: TextLine
536 foo_name: TextLine
537
538- >>> print bar[0]
539+ >>> print bar.version
540 bar
541- >>> dump_entry_interface(bar[1])
542+ >>> dump_entry_interface(bar.object)
543 bar_name: TextLine
544 foo_name: TextLine
545
546@@ -1972,15 +1980,15 @@
547 >>> bar, foo = generate_entry_interfaces(
548 ... IAmbiguousMultiVersion, [], 'bar', 'foo')
549
550- >>> print bar[0]
551+ >>> print bar.version
552 bar
553- >>> dump_entry_interface(bar[1])
554+ >>> dump_entry_interface(bar.object)
555 bar_name: TextLine
556 field1: TextLine
557
558- >>> print foo[0]
559+ >>> print foo.version
560 foo
561- >>> dump_entry_interface(foo[1])
562+ >>> dump_entry_interface(foo.object)
563 bar_name: TextLine
564 foo_name: TextLine
565
566@@ -2039,7 +2047,7 @@
567 Traceback (most recent call last):
568 ...
569 ValueError: Field "field" in interface "IDuplicateEntry":
570- Duplicate annotations for version "beta".
571+ Duplicate definitions for version "beta".
572
573 Or it can happen because you defined the earliest version implicitly
574 using keyword arguments, and then explicitly defined conflicting
575@@ -2091,7 +2099,7 @@
576 ... ('3.0', dict(exported_as='30_name')),
577 ... ('beta', dict(exported_as='unchanging_name')))
578
579- >>> [version for version, tags in
580+ >>> [interface.version for interface in
581 ... generate_entry_interfaces(IUnchangingEntry, [], *versions)]
582 ['beta', '1.0', '2.0', '3.0']
583
584@@ -2317,8 +2325,11 @@
585 But it's not present in the unnamed pre-1.0 version, since it hadn't
586 been defined yet:
587
588- >>> print dictionary.pop()
589- (None, {'type': 'removed_operation'})
590+ >>> pre_10 = dictionary.pop()
591+ >>> print pre_10.version
592+ None
593+ >>> print pre_10.object
594+ {'type': 'removed_operation'}
595
596 The @operation_removed_in_version declaration can also be used to
597 reset a named operation's definition if you need to completely re-do
598
599=== modified file 'src/lazr/restful/tests/test_declarations.py'
600--- src/lazr/restful/tests/test_declarations.py 2011-03-10 14:07:30 +0000
601+++ src/lazr/restful/tests/test_declarations.py 2011-03-17 14:24:12 +0000
602@@ -1,11 +1,28 @@
603 from zope.component import (
604- adapts, getMultiAdapter, getSiteManager, getUtility, provideUtility)
605+ adapts,
606+ getMultiAdapter,
607+ getSiteManager,
608+ getUtility,
609+ )
610 from zope.component.interfaces import ComponentLookupError
611-from zope.interface import alsoProvides, Attribute, implements, Interface
612+from zope.interface import (
613+ Attribute,
614+ implements,
615+ Interface,
616+ )
617 from zope.publisher.interfaces.http import IHTTPRequest
618-from zope.schema import Int, Object, TextLine
619-from zope.security.checker import MultiChecker, ProxyFactory
620-from zope.security.management import endInteraction, newInteraction
621+from zope.schema import (
622+ Int,
623+ TextLine,
624+ )
625+from zope.security.checker import (
626+ MultiChecker,
627+ ProxyFactory,
628+ )
629+from zope.security.management import (
630+ endInteraction,
631+ newInteraction,
632+ )
633
634 from lazr.restful.declarations import (
635 call_with,
636@@ -15,14 +32,17 @@
637 export_write_operation,
638 generate_entry_interfaces,
639 generate_operation_adapter,
640+ LAZR_WEBSERVICE_NAME,
641 mutator_for,
642 operation_for_version,
643 operation_parameters,
644 )
645 from lazr.restful.fields import Reference
646 from lazr.restful.interfaces import (
647- IEntry, IResourceGETOperation, IWebServiceConfiguration,
648- IWebServiceVersion)
649+ IEntry,
650+ IResourceGETOperation,
651+ IWebServiceConfiguration,
652+ )
653 from lazr.restful.marshallers import SimpleFieldMarshaller
654 from lazr.restful.metazcml import (
655 AttemptToContributeToNonExportedInterface,
656@@ -30,13 +50,15 @@
657 find_interfaces_and_contributors,
658 generate_and_register_webservice_operations,
659 )
660-from lazr.restful._resource import EntryAdapterUtility, EntryResource
661-from lazr.restful.testing.webservice import (
662- FakeRequest,
663- TestCaseWithWebServiceFixtures,
664+from lazr.restful._resource import (
665+ EntryAdapterUtility,
666+ EntryResource,
667 )
668+from lazr.restful.testing.webservice import TestCaseWithWebServiceFixtures
669 from lazr.restful.testing.helpers import (
670- create_test_module, TestWebServiceConfiguration, register_test_module)
671+ create_test_module,
672+ register_test_module,
673+ )
674
675
676 class ContributingInterfacesTestCase(TestCaseWithWebServiceFixtures):
677@@ -344,48 +366,159 @@
678 class DummyFieldMarshaller(SimpleFieldMarshaller):
679 adapts(Interface, IHTTPRequest)
680
681+# Classes for TestEntryMultiversion.
682+
683+class INotInitiallyExported(Interface):
684+ # An entry that's not exported in the first version of the web
685+ # service.
686+ export_as_webservice_entry(as_of="2.0")
687+
688+
689+class INotPresentInLaterVersion(Interface):
690+ # An entry that's only exported in the first version of the web
691+ # service.
692+ export_as_webservice_entry(
693+ versioned_annotations=[('2.0', dict(exported=False))])
694+
695+
696+class IHasDifferentNamesInDifferentVersions(Interface):
697+ # An entry that has different names in different versions.
698+ export_as_webservice_entry(
699+ singular_name="octopus", plural_name="octopi",
700+ versioned_annotations=[
701+ ('2.0', dict(singular_name="fish", plural_name="fishes"))])
702+
703+
704+class IHasDifferentSingularNamesInDifferentVersions(Interface):
705+ # An entry that has different names in different versions.
706+ export_as_webservice_entry(
707+ singular_name="frog",
708+ versioned_annotations=[('2.0', dict(singular_name="toad"))])
709+
710+
711+class TestEntryMultiversion(TestCaseWithWebServiceFixtures):
712+ """Test the ability to export an entry only in certain versions."""
713+
714+ def test_not_initially_exported(self):
715+ # INotInitiallyExported is published in version 2.0 but not in 1.0.
716+ interfaces = generate_entry_interfaces(
717+ INotInitiallyExported, [],
718+ *getUtility(IWebServiceConfiguration).active_versions)
719+ self.assertEquals(len(interfaces), 1)
720+ self.assertEquals(interfaces[0].version, '2.0')
721+
722+ def test_not_exported_in_later_version(self):
723+ # INotPresentInLaterVersion is published in version 1.0 but
724+ # not in 2.0.
725+ interfaces = generate_entry_interfaces(
726+ INotPresentInLaterVersion, [],
727+ *getUtility(IWebServiceConfiguration).active_versions)
728+ self.assertEquals(len(interfaces), 1)
729+ tags = interfaces[0][1].getTaggedValue(LAZR_WEBSERVICE_NAME)
730+ self.assertEquals(interfaces[0].version, '1.0')
731+
732+ def test_different_names_in_different_versions(self):
733+ # IHasDifferentNamesInDifferentVersions is called
734+ # octopus/octopi in 1.0, and fish/fishes in 2.0.
735+ interfaces = generate_entry_interfaces(
736+ IHasDifferentNamesInDifferentVersions, [],
737+ *getUtility(IWebServiceConfiguration).active_versions)
738+ interface_10 = interfaces[0].object
739+ tags_10 = interface_10.getTaggedValue(LAZR_WEBSERVICE_NAME)
740+ self.assertEquals('octopus', tags_10['singular'])
741+ self.assertEquals('octopi', tags_10['plural'])
742+
743+ interface_20 = interfaces[1].object
744+ tags_20 = interface_20.getTaggedValue(LAZR_WEBSERVICE_NAME)
745+ self.assertEquals('fish', tags_20['singular'])
746+ self.assertEquals('fishes', tags_20['plural'])
747+
748+ def test_nonexistent_annotation_fails(self):
749+ # You can't define an entry class that includes an unrecognized
750+ # annotation in its versioned_annotations.
751+ try:
752+ class IUsesNonexistentAnnotation(Interface):
753+ export_as_webservice_entry(
754+ versioned_annotations=[
755+ ('2.0', dict(no_such_annotation=True))])
756+ self.fail("Expected ValueError.")
757+ except ValueError, exception:
758+ self.assertEquals(
759+ str(exception),
760+ 'Unrecognized annotation for version "2.0": '
761+ '"no_such_annotation"')
762+
763+ def test_changing_singular_also_changes_plural(self):
764+ # IHasDifferentSingularNamesInDifferentVersions defines the
765+ # singular name 'frog' in 1.0, and 'toad' in 2.0. This test
766+ # makes sure that the plural of 'toad' is not 'frogs'.
767+ interfaces = generate_entry_interfaces(
768+ IHasDifferentSingularNamesInDifferentVersions, [],
769+ *getUtility(IWebServiceConfiguration).active_versions)
770+ interface_10 = interfaces[0].object
771+ tags_10 = interface_10.getTaggedValue(LAZR_WEBSERVICE_NAME)
772+ self.assertEquals('frog', tags_10['singular'])
773+ self.assertEquals('frogs', tags_10['plural'])
774+
775+ interface_20 = interfaces[1].object
776+ tags_20 = interface_20.getTaggedValue(LAZR_WEBSERVICE_NAME)
777+ self.assertEquals('toad', tags_20['singular'])
778+ self.assertEquals('toads', tags_20['plural'])
779+
780
781 # Classes for TestReqireExplicitVersions
782
783+class IEntryExportedWithoutAsOf(Interface):
784+ export_as_webservice_entry()
785+
786+
787+class IEntryExportedWithAsOf(Interface):
788+ export_as_webservice_entry(as_of="1.0")
789+
790+
791+class IEntryExportedAsOfLaterVersion(Interface):
792+ export_as_webservice_entry(as_of="2.0")
793+
794+
795 class IFieldExportedWithoutAsOf(Interface):
796- export_as_webservice_entry()
797+ export_as_webservice_entry(as_of="1.0")
798
799 field = exported(TextLine(), exported=True)
800
801
802 class IFieldExportedToEarliestVersionUsingAsOf(Interface):
803- export_as_webservice_entry()
804+ export_as_webservice_entry(as_of="1.0")
805
806 field = exported(TextLine(), as_of='1.0')
807
808
809 class IFieldExportedToLatestVersionUsingAsOf(Interface):
810- export_as_webservice_entry()
811+ export_as_webservice_entry(as_of="1.0")
812
813 field = exported(TextLine(), as_of='2.0')
814
815
816 class IFieldDefiningAttributesBeforeAsOf(Interface):
817- export_as_webservice_entry()
818+ export_as_webservice_entry(as_of="1.0")
819
820 field = exported(TextLine(), ('1.0', dict(exported=True)),
821 as_of='2.0')
822
823 class IFieldAsOfNonexistentVersion(Interface):
824- export_as_webservice_entry()
825+ export_as_webservice_entry(as_of="1.0")
826
827 field = exported(TextLine(), as_of='nosuchversion')
828
829
830 class IFieldDoubleDefinition(Interface):
831- export_as_webservice_entry()
832+ export_as_webservice_entry(as_of="1.0")
833
834 field = exported(TextLine(), ('2.0', dict(exported_as='name2')),
835 exported_as='name2', as_of='2.0')
836
837
838 class IFieldImplicitOperationDefinition(Interface):
839- export_as_webservice_entry()
840+ export_as_webservice_entry(as_of="1.0")
841
842 @call_with(value="2.0")
843 @operation_for_version('2.0')
844@@ -396,7 +529,7 @@
845
846
847 class IFieldExplicitOperationDefinition(Interface):
848- export_as_webservice_entry()
849+ export_as_webservice_entry(as_of="1.0")
850
851 @call_with(value="2.0")
852 @operation_for_version('2.0')
853@@ -429,14 +562,45 @@
854 except Exception, e:
855 return e
856
857+ def test_entry_exported_with_as_of_succeeds(self):
858+ # An entry exported using as_of is present in the as_of_version
859+ # and in subsequent versions.
860+ interfaces = generate_entry_interfaces(
861+ IEntryExportedWithAsOf, [],
862+ *self.utility.active_versions)
863+ self.assertEquals(len(interfaces), 2)
864+ self.assertEquals(interfaces[0].version, '1.0')
865+ self.assertEquals(interfaces[1].version, '2.0')
866+
867+ def test_entry_exported_as_of_later_version_succeeds(self):
868+ # An entry exported as_of a later version is not present in
869+ # earlier versions.
870+ interfaces = generate_entry_interfaces(
871+ IEntryExportedAsOfLaterVersion, [],
872+ *self.utility.active_versions)
873+ self.assertEquals(len(interfaces), 1)
874+ self.assertEquals(interfaces[0].version, '2.0')
875+
876+ def test_entry_exported_without_as_of_fails(self):
877+ exception = self._assertRaises(
878+ ValueError, generate_entry_interfaces,
879+ IEntryExportedWithoutAsOf, [],
880+ *self.utility.active_versions)
881+ self.assertEquals(
882+ str(exception),
883+ 'Entry "IEntryExportedWithoutAsOf": Exported in version 1.0, '
884+ 'but not by using as_of. The service configuration requires '
885+ 'that you use as_of.')
886+
887+
888 def test_field_exported_as_of_earlier_version_is_exported_in_subsequent_versions(self):
889 # If you export a field as_of version 1.0, it's present in
890 # 1.0's version of the entry and in all subsequent versions.
891 interfaces = generate_entry_interfaces(
892 IFieldExportedToEarliestVersionUsingAsOf, [],
893 *self.utility.active_versions)
894- interface_10 = interfaces[0][1]
895- interface_20 = interfaces[1][1]
896+ interface_10 = interfaces[0].object
897+ interface_20 = interfaces[1].object
898 self.assertEquals(interface_10.names(), ['field'])
899 self.assertEquals(interface_20.names(), ['field'])
900
901@@ -446,8 +610,8 @@
902 interfaces = generate_entry_interfaces(
903 IFieldExportedToLatestVersionUsingAsOf, [],
904 *self.utility.active_versions)
905- interface_10 = interfaces[0][1]
906- interface_20 = interfaces[1][1]
907+ interface_10 = interfaces[0].object
908+ interface_20 = interfaces[1].object
909 self.assertEquals(interface_10.names(), [])
910 self.assertEquals(interface_20.names(), ['field'])
911
912@@ -460,7 +624,7 @@
913 self.assertEquals(
914 str(exception),
915 ('Field "field" in interface "IFieldExportedWithoutAsOf": '
916- 'Field was exported in version 1.0, but not by using as_of. '
917+ 'Exported in version 1.0, but not by using as_of. '
918 'The service configuration requires that you use as_of.')
919 )
920
921@@ -494,7 +658,7 @@
922 self.assertEquals(
923 str(exception),
924 ('Field "field" in interface "IFieldDoubleDefinition": '
925- 'Duplicate annotations for version "2.0".'))
926+ 'Duplicate definitions for version "2.0".'))
927
928 def test_field_with_annotations_that_precede_as_of_fails(self):
929 # You can't provide a dictionary of attributes for a version
930
931=== modified file 'src/lazr/restful/tests/test_utils.py'
932--- src/lazr/restful/tests/test_utils.py 2011-02-18 00:11:51 +0000
933+++ src/lazr/restful/tests/test_utils.py 2011-03-17 14:24:12 +0000
934@@ -5,11 +5,15 @@
935 __metaclass__ = type
936
937 import random
938+import testtools
939 import unittest
940
941 from zope.publisher.browser import TestRequest
942 from zope.security.management import (
943- endInteraction, newInteraction, queryInteraction)
944+ endInteraction,
945+ newInteraction,
946+ queryInteraction,
947+ )
948
949 from lazr.restful.utils import (
950 extract_write_portion,
951@@ -17,6 +21,7 @@
952 is_total_size_link_active,
953 parse_accept_style_header,
954 sorted_named_things,
955+ VersionedDict,
956 )
957
958
959@@ -93,6 +98,111 @@
960 # and tag_request_with_version_name() are tested in test_webservice.py.
961
962
963+class TestVersionedDict(testtools.TestCase):
964+
965+ def setUp(self):
966+ super(TestVersionedDict, self).setUp()
967+ self.dict = VersionedDict()
968+
969+ def test_rename_version_works(self):
970+ # rename_version works when the given version exists.
971+ self.dict.push("original")
972+ self.dict.rename_version("original", "renamed")
973+ self.assertEquals(self.dict.dict_names, ["renamed"])
974+
975+ def test_rename_version_fails_given_nonexistent_version(self):
976+ # rename_version gives KeyError when the given version does
977+ # not exist.
978+ self.dict.push("original")
979+ self.assertRaises(
980+ KeyError, self.dict.rename_version, "not present", "renamed")
981+
982+ def test_dict_for_name_finds_first_dict(self):
983+ # dict_for_name finds a dict with the given name in the stack.
984+ self.dict.push("name1")
985+ self.dict['key'] = 'value1'
986+ self.assertEquals(
987+ self.dict.dict_for_name('name1'), dict(key='value1'))
988+
989+ def test_dict_for_name_finds_first_dict(self):
990+ # If there's more than one dict with a given name,
991+ # dict_for_name() finds the first one.
992+ self.dict.push("name1")
993+ self.dict['key'] = 'value1'
994+ self.dict.push("name2")
995+ self.dict.push("name1")
996+ self.dict['key'] = 'value2'
997+ self.assertEquals(
998+ self.dict.dict_for_name('name1'), dict(key='value1'))
999+
1000+ def test_dict_for_name_returns_None_if_no_such_name(self):
1001+ # If there's no dict with the given name, dict_for_name
1002+ # returns None.
1003+ self.assertEquals(None, self.dict.dict_for_name("name1"))
1004+
1005+ def test_dict_for_name_returns_default_if_no_such_name(self):
1006+ # If there's no dict with the given name, and a default value
1007+ # is provided, dict_for_name returns the default.
1008+ obj = object()
1009+ self.assertEquals(obj, self.dict.dict_for_name("name1", obj))
1010+
1011+ def test_normalize_for_versions_fills_in_blanks(self):
1012+ # `normalize_for_versions` makes sure a VersionedDict has
1013+ # an entry for every one of the given versions.
1014+ self.dict.push("name2")
1015+ self.dict['key'] = 'value'
1016+ self.dict.normalize_for_versions(['name1', 'name2', 'name3'])
1017+ self.assertEquals(
1018+ self.dict.stack,
1019+ [('name1', dict()),
1020+ ('name2', dict(key='value')),
1021+ ('name3', dict(key='value'))])
1022+
1023+ def test_normalize_for_versions_uses_default_dict(self):
1024+ self.dict.push("name2")
1025+ self.dict['key'] = 'value'
1026+ self.dict.normalize_for_versions(
1027+ ['name1', 'name2'], dict(default=True))
1028+ self.assertEquals(
1029+ self.dict.stack,
1030+ [('name1', dict(default=True)),
1031+ ('name2', dict(key='value'))])
1032+
1033+ def test_normalize_for_versions_rejects_nonexistant_versions(self):
1034+ self.dict.push("nosuchversion")
1035+ exception = self.assertRaises(
1036+ ValueError, self.dict.normalize_for_versions, ['name1'])
1037+ self.assertEquals(
1038+ str(exception), 'Unrecognized version "nosuchversion".')
1039+
1040+ def test_normalize_for_versions_rejects_duplicate_versions(self):
1041+ self.dict.push("name1")
1042+ self.dict.push("name1")
1043+ exception = self.assertRaises(
1044+ ValueError, self.dict.normalize_for_versions, ['name1', 'name2'])
1045+ self.assertEquals(
1046+ str(exception), 'Duplicate definitions for version "name1".')
1047+
1048+ def test_normalize_for_versions_rejects_misordered_versions(self):
1049+ self.dict.push("name2")
1050+ self.dict.push("name1")
1051+ exception = self.assertRaises(
1052+ ValueError, self.dict.normalize_for_versions, ['name1', 'name2'])
1053+ self.assertEquals(
1054+ str(exception),
1055+ 'Version "name1" defined after the later version "name2".')
1056+
1057+ def test_error_prefix_prepended_to_exception(self):
1058+ self.dict.push("nosuchversion")
1059+ exception = self.assertRaises(
1060+ ValueError, self.dict.normalize_for_versions, ['name1'],
1061+ error_prefix='Error test: ')
1062+ self.assertEquals(
1063+ str(exception),
1064+ 'Error test: Unrecognized version "nosuchversion".')
1065+
1066+
1067+
1068 class TestParseAcceptStyleHeader(unittest.TestCase):
1069
1070 def test_single_value(self):
1071
1072=== modified file 'src/lazr/restful/utils.py'
1073--- src/lazr/restful/utils.py 2011-02-18 00:11:51 +0000
1074+++ src/lazr/restful/utils.py 2011-03-17 14:24:12 +0000
1075@@ -15,11 +15,13 @@
1076 'smartquote',
1077 'simple_popen2',
1078 'tag_request_with_version_name',
1079+ 'VersionedAnnotations',
1080 'VersionedDict',
1081 ]
1082
1083
1084 import cgi
1085+import collections
1086 import copy
1087 import operator
1088 import re
1089@@ -125,6 +127,10 @@
1090 return [value for quality, value in filtered_accepts]
1091
1092
1093+VersionedObject = collections.namedtuple(
1094+ 'VersionedObject', ['version', 'object'])
1095+
1096+
1097 class VersionedDict(object):
1098 """A stack of named dictionaries.
1099
1100@@ -156,9 +162,9 @@
1101 if empty or len(self.stack) == 0:
1102 dictionary = {}
1103 else:
1104- stack_top = self.stack[-1][1]
1105+ stack_top = self.stack[-1].object
1106 dictionary = copy.deepcopy(stack_top)
1107- self.stack.append((name, dict(dictionary)))
1108+ self.stack.append(VersionedObject(name, dict(dictionary)))
1109
1110 def pop(self):
1111 """Pop a tuple representing a dictionary from the stack.
1112@@ -169,19 +175,98 @@
1113 """
1114 return self.stack.pop()
1115
1116+ def dict_for_name(self, version, default=None):
1117+ """Find the first dict for the given version."""
1118+ matches = [item.object for item in self.stack
1119+ if item.version == version]
1120+ if len(matches) == 0:
1121+ return default
1122+ return matches[0]
1123+
1124+ def normalize_for_versions(self, versions, default_dictionary=None,
1125+ error_prefix=''):
1126+ """Fill out the stack with something for every given version.
1127+
1128+ :param versions: A list of names for dictionaries. By the time
1129+ this method completes, the value of `dict_names` will be the
1130+ same as this list. 'Earlier' versions are presumed to come
1131+ before 'later' versions, but this only affects the error
1132+ message given when annotations are discovered to be out of
1133+ order.
1134+
1135+ :param default_dictionary: A dictionary to use for versions that
1136+ can't inherit values from some other dictionary.
1137+
1138+ :param error_prefix: A string to prepend to errors when
1139+ raising exceptions.
1140+
1141+ :raise ValueError: If the existing dictionary stack includes
1142+ names not present in `versions`, if the names in the stack
1143+ are present in an order other than the one given in
1144+ `versions`, or if any names in the stack are duplicated.
1145+ """
1146+ new_stack = []
1147+
1148+ # Do some error checking.
1149+ max_index = -1
1150+ for version, tags in self.stack:
1151+ try:
1152+ version_index = versions.index(version)
1153+ except ValueError:
1154+ raise ValueError(
1155+ error_prefix + 'Unrecognized version "%s".' % version)
1156+ if version_index == max_index:
1157+ raise ValueError(
1158+ error_prefix + 'Duplicate definitions for version '
1159+ '"%s".' % version)
1160+ if version_index < max_index:
1161+ raise ValueError(
1162+ error_prefix + 'Version "%s" defined after '
1163+ 'the later version "%s".' % (version, versions[max_index]))
1164+ max_index = version_index
1165+
1166+ # We now know that the versions present in the dictionary are
1167+ # an ordered subset of the versions in `versions`. All we have
1168+ # to do now is fill in the blanks.
1169+ most_recent_dict = default_dictionary or {}
1170+ for version in versions:
1171+ existing_dict = self.dict_for_name(version)
1172+ if existing_dict is None:
1173+ # This version has no dictionary of its own. Use a copy of
1174+ # the most recent dictionary.
1175+ new_stack.append(
1176+ VersionedObject(
1177+ version, copy.deepcopy(most_recent_dict)))
1178+ else:
1179+ # This version has a dictionary. Use it.
1180+ new_stack.append(
1181+ VersionedObject(version, existing_dict))
1182+ most_recent_dict = existing_dict
1183+ self.stack = new_stack
1184+
1185 @property
1186 def dict_names(self):
1187 """Return the names of dictionaries in the stack."""
1188- return [item[0] for item in self.stack if item is not None]
1189+ return [item.version for item in self.stack if item is not None]
1190
1191 @property
1192 def is_empty(self):
1193 """Is the stack empty?"""
1194 return len(self.stack) == 0
1195
1196+ def rename_version(self, old_name, new_name):
1197+ """Change the name of a version."""
1198+ for index, pair in enumerate(self.stack):
1199+ if pair.version == old_name:
1200+ self.stack[index] = VersionedObject(
1201+ new_name, pair.object)
1202+ break
1203+ else:
1204+ raise KeyError(old_name)
1205+
1206 def setdefault(self, key, value):
1207 """Get a from the top of the stack, setting it if not present."""
1208- return self.stack[-1][1].setdefault(key, value)
1209+ return self.stack[-1].object.setdefault(key, value)
1210
1211 def __contains__(self, key):
1212 """Check whether a key is visible in the stack."""
1213@@ -191,31 +276,31 @@
1214 """Look up an item somewhere in the stack."""
1215 if self.is_empty:
1216 raise KeyError(key)
1217- return self.stack[-1][1][key]
1218+ return self.stack[-1].object[key]
1219
1220 def get(self, key, default=None):
1221 """Look up an item somewhere in the stack, with default fallback."""
1222 if self.is_empty:
1223 return default
1224- return self.stack[-1][1].get(key, default)
1225+ return self.stack[-1].object.get(key, default)
1226
1227 def items(self):
1228 """Return a merged view of the items in the dictionary."""
1229 if self.is_empty:
1230 raise IndexError("Stack is empty")
1231- return self.stack[-1][1].items()
1232+ return self.stack[-1].object.items()
1233
1234 def __setitem__(self, key, value):
1235 """Set a value in the dict at the top of the stack."""
1236 if self.is_empty:
1237 raise IndexError("Stack is empty")
1238- self.stack[-1][1][key] = value
1239+ self.stack[-1].object[key] = value
1240
1241 def __delitem__(self, key):
1242 """Delete a value from the dict at the top of the stack."""
1243 if self.is_empty:
1244 raise IndexError("Stack is empty")
1245- del self.stack[-1][1][key]
1246+ del self.stack[-1].object[key]
1247
1248
1249 def implement_from_dict(class_name, interface, values, superclass=object):

Subscribers

People subscribed via source and target branches