Merge lp:~leonardr/lazr.restful/entry-introduced-in-version into lp:lazr.restful
- entry-introduced-in-version
- Merge into trunk
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 |
Related bugs: |
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 |
Commit message
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_
To see how this works, look at the test classes defined in test_declaratio
class INotInitiallyEx
# An entry that's not exported in the first version of the web
# service.
export_
class INotPresentInLa
# An entry that's only exported in the first version of the web
# service.
export_
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_
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_
I had a bad cold while I wrote this, but I think it's not too crazy.
Tim Penhey (thumper) wrote : | # |
Does lazr.restful use testtools for the TestCase?
If it does, assertRaises returns the exception, so
test_non_
simplified.
It seems that normalize_
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_
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.
Leonard Richardson (leonardr) wrote : | # |
> Does lazr.restful use testtools for the TestCase?
>
> If it does, assertRaises returns the exception, so
> test_non_
> 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.
Leonard Richardson (leonardr) wrote : | # |
Here's my incremental diff:
http://
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_interface s and generate_ entry_adapters now return VersionedObject namedtuples. - 203. By Leonard Richardson
-
Consistently use VersionedObject instead of VersionedDict.
Leonard Richardson (leonardr) wrote : | # |
Here's a better diff. I use one VersionedObject namedtuple class for all purposes: interfaces, adapters, and annotation dictionaries.
Ian Booth (wallyworld) wrote : | # |
Thanks for incorporating the suggested changes. The use of namedtuple does make the code more readable.
Preview Diff
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): |
Hi Leonard,
This looks good to me. I have some small fixme's:
diff line 36: param should be "versioned_ annotations" not "annotations" 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.
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.
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_interface s. So
interfaces = generate_ entry_interface s(...)
... interfaces[0][0]
... interfaces[0][1]
becomes
interfaces = generate_ entry_interface s(...) 0].version 0].interface
... interfaces[
... interfaces[