Merge lp:~leonardr/lazr.restful/multiversion-mutators into lp:lazr.restful
- multiversion-mutators
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/multiversion-mutators |
Merge into: | lp:lazr.restful |
Diff against target: |
303 lines (+192/-20) 2 files modified
src/lazr/restful/declarations.py (+70/-11) src/lazr/restful/docs/webservice-declarations.txt (+122/-9) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/multiversion-mutators |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Review via email: mp+18702@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
Revision history for this message
Paul Hummer (rockstar) wrote : | # |
Leonard, thanks for making the pagetests so detailed. It makes reviewing easier, since I hop back and forth between test and code when I have questions. I'm also glad to see the tests that were XXX missing from a previous branch are in this one.
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 2010-02-04 22:23:54 +0000 |
3 | +++ src/lazr/restful/declarations.py 2010-02-05 15:08:15 +0000 |
4 | @@ -8,6 +8,7 @@ |
5 | 'ENTRY_TYPE', |
6 | 'FIELD_TYPE', |
7 | 'LAZR_WEBSERVICE_EXPORTED', |
8 | + 'LAZR_WEBSERVICE_MUTATORS', |
9 | 'OPERATION_TYPES', |
10 | 'REQUEST_USER', |
11 | 'cache_for', |
12 | @@ -63,6 +64,7 @@ |
13 | make_identifier_safe, VersionedDict) |
14 | |
15 | LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS |
16 | +LAZR_WEBSERVICE_MUTATORS = '%s.exported.mutators' % LAZR_WEBSERVICE_NS |
17 | COLLECTION_TYPE = 'collection' |
18 | ENTRY_TYPE = 'entry' |
19 | FIELD_TYPE = 'field' |
20 | @@ -220,6 +222,13 @@ |
21 | |
22 | # Now we can annotate the field object with the VersionedDict. |
23 | field.setTaggedValue(LAZR_WEBSERVICE_EXPORTED, annotation_stack) |
24 | + |
25 | + # We track the field's mutator information separately because it's |
26 | + # defined in the named operations, not in the fields. The last |
27 | + # thing we want to do is try to insert a foreign value into an |
28 | + # already create annotation stack. |
29 | + field.setTaggedValue(LAZR_WEBSERVICE_MUTATORS, {}) |
30 | + |
31 | return field |
32 | |
33 | |
34 | @@ -531,13 +540,19 @@ |
35 | "non-fixed argument. %s takes %d." % |
36 | (method.__name__, len(free_params))) |
37 | |
38 | - field_annotations = self.field.queryTaggedValue( |
39 | - LAZR_WEBSERVICE_EXPORTED) |
40 | - if 'mutated_by' in field_annotations: |
41 | - raise TypeError("A field can only have one mutator method; " |
42 | - "%s makes two." % method.__name__ ) |
43 | - field_annotations['mutated_by'] = method |
44 | - field_annotations['mutated_by_annotations'] = annotations |
45 | + # We need to keep mutator annotations in a separate dictionary |
46 | + # from the field's main annotations because we're not |
47 | + # processing the field. We're processing a named operation, |
48 | + # and we have no idea where the named operation's current |
49 | + # version fits into the field's annotations. |
50 | + version, method_annotations = annotations.stack[-1] |
51 | + mutator_annotations = self.field.queryTaggedValue( |
52 | + LAZR_WEBSERVICE_MUTATORS) |
53 | + if version in mutator_annotations: |
54 | + raise TypeError( |
55 | + "A field can only have one mutator method for version %s; " |
56 | + "%s makes two." % (_version_name(version), method.__name__ )) |
57 | + mutator_annotations[version] = (method, dict(method_annotations)) |
58 | |
59 | |
60 | def _free_parameters(method, annotations): |
61 | @@ -879,8 +894,15 @@ |
62 | tags = tags[0] |
63 | if tags.get('exported') is False: |
64 | continue |
65 | - readonly = (field.readonly |
66 | - and tags.get('mutated_by', None) is None) |
67 | + mutator_annotations = field.queryTaggedValue( |
68 | + LAZR_WEBSERVICE_MUTATORS) |
69 | + error_prefix = ( |
70 | + 'Duplicate mutator for interface "%s", field "%s":' % ( |
71 | + interface.__name__, field.__name__)) |
72 | + mutated_by, mutated_by_annotations = _get_by_version( |
73 | + mutator_annotations, version, versions[0], |
74 | + error_prefix, (None, {})) |
75 | + readonly = (field.readonly and mutated_by is None) |
76 | attrs[tags['as']] = copy_field( |
77 | field, __name__=tags['as'], readonly=readonly) |
78 | class_name = _versioned_class_name( |
79 | @@ -919,6 +941,7 @@ |
80 | adapters_by_version = {} |
81 | for name, field in getFields(content_interface).items(): |
82 | tag_stack = field.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) |
83 | + mutator_annotations = field.queryTaggedValue(LAZR_WEBSERVICE_MUTATORS) |
84 | if tag_stack is None: |
85 | continue |
86 | for tags_version, tags in tag_stack.stack: |
87 | @@ -931,11 +954,16 @@ |
88 | # this version's adapter class dictionary. |
89 | if tags.get('exported') is False: |
90 | continue |
91 | - mutator = tags.get('mutated_by', None) |
92 | + error_prefix = ( |
93 | + 'Duplicate mutator for interface "%s", field "%s":' % ( |
94 | + content_interface.__name__, field.__name__)) |
95 | + mutator, annotations = _get_by_version( |
96 | + mutator_annotations, tags_version, webservice_interfaces[0][0], |
97 | + error_prefix, (None, {})) |
98 | + |
99 | if mutator is None: |
100 | property = Passthrough(name, 'context') |
101 | else: |
102 | - annotations = tags['mutated_by_annotations'] |
103 | property = PropertyWithMutator( |
104 | name, 'context', mutator, annotations) |
105 | adapter_dict[tags['as']] = property |
106 | @@ -1291,3 +1319,34 @@ |
107 | version = "__Earliest" |
108 | name = "%s_%s" % (base_name, version.encode('utf8')) |
109 | return make_identifier_safe(name) |
110 | + |
111 | + |
112 | +def _get_by_version(dictionary, version, earliest_version, |
113 | + error_prefix, default=None): |
114 | + """Pull from a dictionary using `version` as the key. |
115 | + |
116 | + Under normal circumstances, this will be an ordinary lookup. But |
117 | + some values for the earliest version are stored under None |
118 | + (because the name of the earliest version wasn't known at the |
119 | + time). This helper function knows to look again under None if |
120 | + a lookup for the earliest version fails. |
121 | + |
122 | + It also knows to raise an exception signaling a conflict if a |
123 | + value is present under the earliest version *and* under None. |
124 | + |
125 | + :param dictionary: The dictionary to use for the lookup. |
126 | + :param version: The dictionary key. |
127 | + :param earliest_version: The version that might also be filed under None. |
128 | + :param default: A default value to use if the lookup fails. |
129 | + """ |
130 | + value = dictionary.get(version, None) |
131 | + if version == earliest_version: |
132 | + earliest_value = dictionary.get(None, None) |
133 | + if value is not None and earliest_value is not None: |
134 | + raise ValueError( |
135 | + error_prefix + " Both implicit and explicit definitions found " |
136 | + "for earliest version %s." % version) |
137 | + value = value or earliest_value |
138 | + if value is None: |
139 | + value = default |
140 | + return value |
141 | |
142 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' |
143 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-02-04 21:22:50 +0000 |
144 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-02-05 15:08:15 +0000 |
145 | @@ -1465,8 +1465,8 @@ |
146 | ... pass |
147 | Traceback (most recent call last): |
148 | ... |
149 | - TypeError: A field can only have one mutator method; set_value_2 |
150 | - makes two. |
151 | + TypeError: A field can only have one mutator method for version |
152 | + (earliest version); set_value_2 makes two. |
153 | |
154 | Caching |
155 | ======= |
156 | @@ -1622,6 +1622,11 @@ |
157 | Entries |
158 | ------- |
159 | |
160 | +The singular and plural name of an entry never changes between |
161 | +versions, because the names are a property of the original |
162 | +interface. But the published fields can change or be renamed from |
163 | +version to version. |
164 | + |
165 | Here's a data model interface defining four fields which are published |
166 | in some versions and not others, and which may have different names in |
167 | different versions. |
168 | @@ -1803,11 +1808,9 @@ |
169 | AttributeError: 'MultiVersionEntryEntry_3_0Adapter' object has no |
170 | attribute 'new_in_10' |
171 | |
172 | - |
173 | -[XXX leonardr I'll add these tests in my next branch; this branch is |
174 | -already way too large. |
175 | - |
176 | - The singular and plural name of an entry will not change between versions. |
177 | +[XXX leonardr These tests need to be done in pagetests, since they |
178 | + test the behavior of the webservice rather than whether certain |
179 | + combinations of annotations are valid. |
180 | |
181 | If a field has a mutator in version n, later versions will inherit the |
182 | same mutator. |
183 | @@ -2245,8 +2248,118 @@ |
184 | >>> attrs['return_type'] |
185 | <lazr.restful.fields.CollectionField object...> |
186 | |
187 | -[XXX leonardr mutator_for modifies the field, not the method, so it |
188 | -won't work until I add multiversion support for fields.] |
189 | +Mutator operations |
190 | +****************** |
191 | + |
192 | +Different versions can define different mutator methods for the same field. |
193 | + |
194 | + >>> class IDifferentMutators(Interface): |
195 | + ... export_as_webservice_entry() |
196 | + ... |
197 | + ... field = exported(TextLine(readonly=True)) |
198 | + ... |
199 | + ... @mutator_for(field) |
200 | + ... @export_write_operation() |
201 | + ... @operation_for_version('beta') |
202 | + ... @operation_parameters(new_value=TextLine()) |
203 | + ... def set_value(new_value): |
204 | + ... pass |
205 | + ... |
206 | + ... @mutator_for(field) |
207 | + ... @export_write_operation() |
208 | + ... @operation_for_version('1.0') |
209 | + ... @operation_parameters(new_value=TextLine()) |
210 | + ... def set_value_2(new_value): |
211 | + ... pass |
212 | + |
213 | + >>> ignored = generate_entry_interfaces( |
214 | + ... IDifferentMutators, 'beta', '1.0') |
215 | + |
216 | +But you can't define two mutators for the same field in the same version. |
217 | + |
218 | + >>> class IDuplicateMutator(Interface): |
219 | + ... export_as_webservice_entry() |
220 | + ... |
221 | + ... field = exported(TextLine(readonly=True)) |
222 | + ... |
223 | + ... @mutator_for(field) |
224 | + ... @export_write_operation() |
225 | + ... @operation_for_version('1.0') |
226 | + ... @operation_parameters(new_value=TextLine()) |
227 | + ... def set_value(new_value): |
228 | + ... pass |
229 | + ... |
230 | + ... @mutator_for(field) |
231 | + ... @export_write_operation() |
232 | + ... @operation_for_version('1.0') |
233 | + ... @operation_parameters(new_value=TextLine()) |
234 | + ... def set_value_2(new_value): |
235 | + ... pass |
236 | + Traceback (most recent call last): |
237 | + ... |
238 | + TypeError: A field can only have one mutator method for version |
239 | + 1.0; set_value_2 makes two. |
240 | + |
241 | +Here's a case that's a little trickier. You'll also get an error if |
242 | +you implicitly define a mutator for the earliest version (without |
243 | +giving its name), and then define another one explicitly (giving the |
244 | +name of the earliest version.) |
245 | + |
246 | + >>> class IImplicitAndExplicitMutator(Interface): |
247 | + ... export_as_webservice_entry() |
248 | + ... |
249 | + ... field = exported(TextLine(readonly=True)) |
250 | + ... |
251 | + ... @mutator_for(field) |
252 | + ... @export_write_operation() |
253 | + ... @operation_for_version('beta') |
254 | + ... @operation_parameters(new_value=TextLine()) |
255 | + ... def set_value_2(new_value): |
256 | + ... pass |
257 | + ... |
258 | + ... @mutator_for(field) |
259 | + ... @export_write_operation() |
260 | + ... @operation_parameters(new_value=TextLine()) |
261 | + ... def set_value_2(new_value): |
262 | + ... pass |
263 | + |
264 | + >>> generate_entry_interfaces( |
265 | + ... IImplicitAndExplicitMutator, 'beta', '1.0') |
266 | + Traceback (most recent call last): |
267 | + ... |
268 | + ValueError: Duplicate mutator for interface |
269 | + "IImplicitAndExplicitMutator", field "field": Both implicit and |
270 | + explicit definitions found for earliest version beta. |
271 | + |
272 | +This error isn't detected until you try to generate the entry |
273 | +interfaces, because until that point lazr.restful doesn't know that |
274 | +'beta' is the earliest version. If the earliest version was 'alpha', |
275 | +the IImplicitAndExplicitMutator class would be valid. |
276 | + |
277 | +(Again, to test this hypothesis, we need to re-define the class, |
278 | +because the generate_entry_interfaces call modified the original |
279 | +class's annotations in place.) |
280 | + |
281 | + >>> class IImplicitAndExplicitMutator(Interface): |
282 | + ... export_as_webservice_entry() |
283 | + ... |
284 | + ... field = exported(TextLine(readonly=True)) |
285 | + ... |
286 | + ... @mutator_for(field) |
287 | + ... @export_write_operation() |
288 | + ... @operation_for_version('beta') |
289 | + ... @operation_parameters(new_value=TextLine()) |
290 | + ... def set_value_2(new_value): |
291 | + ... pass |
292 | + ... |
293 | + ... @mutator_for(field) |
294 | + ... @export_write_operation() |
295 | + ... @operation_parameters(new_value=TextLine()) |
296 | + ... def set_value_2(new_value): |
297 | + ... pass |
298 | + |
299 | + >>> ignored = generate_entry_interfaces( |
300 | + ... IImplicitAndExplicitMutator, 'alpha', 'beta', '1.0') |
301 | |
302 | Destructor operations |
303 | ********************* |
This diff makes mutators version-aware. It lets you define different mutators for different versions, and it prevents you from defining more than one mutator for any particular version.
The complication here is that the @mutator_for annotation is attached to the mutator method, but it modifies a field. Before multi-version, @mutator_for just grabbed the field's annotation dictionary and modified it. But now, "modifying the field's annotation dictionary" means modifying the way the field is published *in the most recent version*. We need to modify the way the field is published in a *specific* version. But we have no guarantee that the field already has an existing set of annotations for that version, and if it's not in the stack already we have no idea whether that version comes before or after other versions.
Here's an example. (I don't have a test like this because it doesn't add any code coverage, and I don't think it's necessary to write a test to explain the internal implementation of something, but it's the best way to explain the problem.) Let's say I publish an entry like this:
field = exported( TextLine( ), ('foo', dict(exported_ as='foofield' ),
(' bar', dict(exported_ as='barfield' ))
@mutator_ for(field) write_operation () for_version( 'baz')
@export_
@operation_
def mutator(value):
...
When I process the 'mutator' method I need to modify the annotations on the 'field' field. That field's LAZR_WEBSERVICE _EXPORTED annotation stack looks the same as it did in the exported() call:
[('foo', dict(exported_ as='foofield' ), as='barfield' )]
('bar', dict(exported_
Where does 'baz' go? There is *no way* to know. The decision has to be deferred until generate_ entry_interface s, when we have an ordered list of the versions. So this 'baz' thing can't go into LAZR_WEBSERVICE _EXPORTED at all. That's why I created a separate annotation dictionary just for mutators, LAZR_WEBSERVICE _MUTATORS. This is a regular dictionary (not a VersionedDict) that maps versions to (mutator, mutator_ annotations) 2-tuples. When we encounter @mutator_for, the stuff that was going to go into LAZR_WEBSERVICE _EXPORTED gets put into here instead. Later on, in generate_ entry_interface s and generate_ entry_adapters, when we need to do per-version error checking or generate a per-version Property, we pull the version-indexed value out of LAZR_WEBSERVICE _MUTATORS.