Merge lp:~leonardr/lazr.restful/multiversion-destructors into lp:lazr.restful
- multiversion-destructors
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/multiversion-destructors |
Merge into: | lp:lazr.restful |
Diff against target: |
227 lines (+94/-29) 2 files modified
src/lazr/restful/declarations.py (+37/-23) src/lazr/restful/docs/webservice-declarations.txt (+57/-6) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/multiversion-destructors |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | code | Approve | |
Review via email: mp+18643@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 : | # |
Based on previous knowledge from reviewing the dependent branch, this branch is a bit easier to grok. Once again, your doctests make this patch easier to understand.
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 21:50:06 +0000 | |||
3 | +++ src/lazr/restful/declarations.py 2010-02-04 21:53:10 +0000 | |||
4 | @@ -285,11 +285,10 @@ | |||
5 | 285 | 'collection_default_content', {}) | 285 | 'collection_default_content', {}) |
6 | 286 | 286 | ||
7 | 287 | if version in default_content_methods: | 287 | if version in default_content_methods: |
8 | 288 | if version is None: | ||
9 | 289 | version = "(earliest version)" | ||
10 | 290 | raise TypeError( | 288 | raise TypeError( |
11 | 291 | "Only one method can be marked with " | 289 | "Only one method can be marked with " |
13 | 292 | "@collection_default_content for version '%s'." % version) | 290 | "@collection_default_content for version '%s'." % ( |
14 | 291 | _version_name(version))) | ||
15 | 293 | self.version = version | 292 | self.version = version |
16 | 294 | self.params = params | 293 | self.params = params |
17 | 295 | 294 | ||
18 | @@ -424,11 +423,6 @@ | |||
19 | 424 | # Make sure that each version of the web service defines | 423 | # Make sure that each version of the web service defines |
20 | 425 | # a self-consistent view of this method. | 424 | # a self-consistent view of this method. |
21 | 426 | for version, annotations in annotation_stack.stack: | 425 | for version, annotations in annotation_stack.stack: |
22 | 427 | if version is None: | ||
23 | 428 | # Create a human-readable name for the earliest version | ||
24 | 429 | # without trying to look it up. | ||
25 | 430 | version = "(earliest version)" | ||
26 | 431 | |||
27 | 432 | if annotations['type'] == REMOVED_OPERATION_TYPE: | 426 | if annotations['type'] == REMOVED_OPERATION_TYPE: |
28 | 433 | # The method is published in other versions of the web | 427 | # The method is published in other versions of the web |
29 | 434 | # service, but not in this one. Don't try to validate this | 428 | # service, but not in this one. Don't try to validate this |
30 | @@ -456,15 +450,15 @@ | |||
31 | 456 | raise TypeError( | 450 | raise TypeError( |
32 | 457 | 'method "%s" doesn\'t have the following exported ' | 451 | 'method "%s" doesn\'t have the following exported ' |
33 | 458 | 'parameters in version "%s": %s.' % ( | 452 | 'parameters in version "%s": %s.' % ( |
35 | 459 | method.__name__, version, | 453 | method.__name__, _version_name(version), |
36 | 460 | ", ".join(sorted(undefined_params)))) | 454 | ", ".join(sorted(undefined_params)))) |
37 | 461 | missing_params = set( | 455 | missing_params = set( |
38 | 462 | info['required']).difference(exported_params) | 456 | info['required']).difference(exported_params) |
39 | 463 | if missing_params: | 457 | if missing_params: |
40 | 464 | raise TypeError( | 458 | raise TypeError( |
44 | 465 | 'method "%s" is missing exported parameter definitions ' | 459 | 'method "%s" is missing definitions for parameter(s) ' |
45 | 466 | 'in version "%s": %s' % ( | 460 | 'exported in version "%s": %s' % ( |
46 | 467 | method.__name__, version, | 461 | method.__name__, _version_name(version), |
47 | 468 | ", ".join(sorted(missing_params)))) | 462 | ", ".join(sorted(missing_params)))) |
48 | 469 | 463 | ||
49 | 470 | _update_default_and_required_params(annotations['params'], info) | 464 | _update_default_and_required_params(annotations['params'], info) |
50 | @@ -784,21 +778,29 @@ | |||
51 | 784 | """ | 778 | """ |
52 | 785 | type = "destructor" | 779 | type = "destructor" |
53 | 786 | 780 | ||
55 | 787 | def annotate_method(self, method, annotations): | 781 | def annotate_method(self, method, annotation_stack): |
56 | 788 | """See `_method_annotator`. | 782 | """See `_method_annotator`. |
57 | 789 | 783 | ||
58 | 790 | Store information about the mutator method with the method. | 784 | Store information about the mutator method with the method. |
59 | 785 | |||
60 | 786 | Every version must have a self-consistent set of annotations. | ||
61 | 791 | """ | 787 | """ |
62 | 788 | super(export_destructor_operation, self).annotate_method( | ||
63 | 789 | method, annotation_stack) | ||
64 | 792 | # The mutator method must take no arguments, not counting | 790 | # The mutator method must take no arguments, not counting |
65 | 793 | # arguments with values fixed by call_with(). | 791 | # arguments with values fixed by call_with(). |
66 | 794 | signature = fromFunction(method).getSignatureInfo() | 792 | signature = fromFunction(method).getSignatureInfo() |
74 | 795 | free_params = _free_parameters(method, annotations) | 793 | for version, annotations in annotation_stack.stack: |
75 | 796 | if len(free_params) != 0: | 794 | if annotations['type'] == REMOVED_OPERATION_TYPE: |
76 | 797 | raise TypeError("A destructor method must take no " | 795 | continue |
77 | 798 | "non-fixed arguments; %s takes %d." % | 796 | free_params = _free_parameters(method, annotations) |
78 | 799 | (method.__name__, len(free_params))) | 797 | if len(free_params) != 0: |
79 | 800 | super(export_destructor_operation, self).annotate_method( | 798 | raise TypeError( |
80 | 801 | method, annotations) | 799 | "A destructor method must take no non-fixed arguments. " |
81 | 800 | 'In version %s, the "%s" method takes %d: "%s".' % ( | ||
82 | 801 | _version_name(version), method.__name__, | ||
83 | 802 | len(free_params), '", "'.join(free_params)) | ||
84 | 803 | ) | ||
85 | 802 | 804 | ||
86 | 803 | 805 | ||
87 | 804 | def _check_tagged_interface(interface, type): | 806 | def _check_tagged_interface(interface, type): |
88 | @@ -845,12 +847,11 @@ | |||
89 | 845 | if annotations.get('type') == export_destructor_operation.type: | 847 | if annotations.get('type') == export_destructor_operation.type: |
90 | 846 | destructor = destructor_for_version.get(version) | 848 | destructor = destructor_for_version.get(version) |
91 | 847 | if destructor is not None: | 849 | if destructor is not None: |
92 | 848 | if version is None: | ||
93 | 849 | version = "(earliest)" | ||
94 | 850 | raise TypeError( | 850 | raise TypeError( |
95 | 851 | 'An entry can only have one destructor method for ' | 851 | 'An entry can only have one destructor method for ' |
96 | 852 | 'version %s; %s and %s make two.' % ( | 852 | 'version %s; %s and %s make two.' % ( |
98 | 853 | version, method.__name__, destructor.__name__)) | 853 | _version_name(version), method.__name__, |
99 | 854 | destructor.__name__)) | ||
100 | 854 | destructor_for_version[version] = method | 855 | destructor_for_version[version] = method |
101 | 855 | 856 | ||
102 | 856 | # Next, we'll normalize each published field. A normalized field | 857 | # Next, we'll normalize each published field. A normalized field |
103 | @@ -1267,6 +1268,19 @@ | |||
104 | 1267 | versioned_dict.stack = new_stack | 1268 | versioned_dict.stack = new_stack |
105 | 1268 | return field | 1269 | return field |
106 | 1269 | 1270 | ||
107 | 1271 | |||
108 | 1272 | def _version_name(version): | ||
109 | 1273 | """Return a human-readable version name. | ||
110 | 1274 | |||
111 | 1275 | If `version` is None (indicating the as-yet-unknown earliest | ||
112 | 1276 | version), returns "(earliest version)". Otherwise returns the version | ||
113 | 1277 | name. | ||
114 | 1278 | """ | ||
115 | 1279 | if version is None: | ||
116 | 1280 | return "(earliest version)" | ||
117 | 1281 | return version | ||
118 | 1282 | |||
119 | 1283 | |||
120 | 1270 | def _versioned_class_name(base_name, version): | 1284 | def _versioned_class_name(base_name, version): |
121 | 1271 | """Create a class name incorporating the given version string.""" | 1285 | """Create a class name incorporating the given version string.""" |
122 | 1272 | if version is None: | 1286 | if version is None: |
123 | 1273 | 1287 | ||
124 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' | |||
125 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-02-03 20:59:22 +0000 | |||
126 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-02-04 21:53:10 +0000 | |||
127 | @@ -534,8 +534,8 @@ | |||
128 | 534 | ... def a_method(param1, param2, param3, param4): pass | 534 | ... def a_method(param1, param2, param3, param4): pass |
129 | 535 | Traceback (most recent call last): | 535 | Traceback (most recent call last): |
130 | 536 | ... | 536 | ... |
133 | 537 | TypeError: method "a_method" is missing exported parameter definitions | 537 | TypeError: method "a_method" is missing definitions for parameter(s) |
134 | 538 | in version "(earliest version)": param3, param4 | 538 | exported in version "(earliest version)": param3, param4 |
135 | 539 | 539 | ||
136 | 540 | Defining a parameter not available on the method also results in an | 540 | Defining a parameter not available on the method also results in an |
137 | 541 | error: | 541 | error: |
138 | @@ -1281,12 +1281,14 @@ | |||
139 | 1281 | ... text = exported(TextLine(readonly=True)) | 1281 | ... text = exported(TextLine(readonly=True)) |
140 | 1282 | ... | 1282 | ... |
141 | 1283 | ... @export_destructor_operation() | 1283 | ... @export_destructor_operation() |
142 | 1284 | ... @operation_parameters(argument=TextLine()) | ||
143 | 1284 | ... def destroy(argument): | 1285 | ... def destroy(argument): |
144 | 1285 | ... pass | 1286 | ... pass |
145 | 1286 | Traceback (most recent call last): | 1287 | Traceback (most recent call last): |
146 | 1287 | ... | 1288 | ... |
149 | 1288 | TypeError: A destructor method must take no non-fixed arguments; | 1289 | TypeError: A destructor method must take no non-fixed arguments. |
150 | 1289 | destroy takes 1. | 1290 | In version (earliest version), the "destroy" method takes 1: |
151 | 1291 | "argument". | ||
152 | 1290 | 1292 | ||
153 | 1291 | >>> class IHasText(Interface): | 1293 | >>> class IHasText(Interface): |
154 | 1292 | ... export_as_webservice_entry() | 1294 | ... export_as_webservice_entry() |
155 | @@ -1298,7 +1300,6 @@ | |||
156 | 1298 | ... pass | 1300 | ... pass |
157 | 1299 | >>> ignored = generate_entry_interfaces(IHasText, 'beta') | 1301 | >>> ignored = generate_entry_interfaces(IHasText, 'beta') |
158 | 1300 | 1302 | ||
159 | 1301 | |||
160 | 1302 | An entry cannot have more than one destructor. | 1303 | An entry cannot have more than one destructor. |
161 | 1303 | 1304 | ||
162 | 1304 | >>> from lazr.restful.declarations import export_destructor_operation | 1305 | >>> from lazr.restful.declarations import export_destructor_operation |
163 | @@ -1317,7 +1318,7 @@ | |||
164 | 1317 | Traceback (most recent call last): | 1318 | Traceback (most recent call last): |
165 | 1318 | ... | 1319 | ... |
166 | 1319 | TypeError: An entry can only have one destructor method for | 1320 | TypeError: An entry can only have one destructor method for |
168 | 1320 | version (earliest); destroy and destroy2 make two. | 1321 | version (earliest version); destroy and destroy2 make two. |
169 | 1321 | 1322 | ||
170 | 1322 | 1323 | ||
171 | 1323 | Mutators | 1324 | Mutators |
172 | @@ -2247,6 +2248,56 @@ | |||
173 | 2247 | [XXX leonardr mutator_for modifies the field, not the method, so it | 2248 | [XXX leonardr mutator_for modifies the field, not the method, so it |
174 | 2248 | won't work until I add multiversion support for fields.] | 2249 | won't work until I add multiversion support for fields.] |
175 | 2249 | 2250 | ||
176 | 2251 | Destructor operations | ||
177 | 2252 | ********************* | ||
178 | 2253 | |||
179 | 2254 | A destructor can be published in different ways in different versions, | ||
180 | 2255 | but the restrictions on destructor arguments are enforced separately | ||
181 | 2256 | for each version. | ||
182 | 2257 | |||
183 | 2258 | Here, the destructor fixes a value for the 'fixed2' argument in the | ||
184 | 2259 | earliest version, but not in '1.0'. This is fine: the 1.0 value for | ||
185 | 2260 | 'fixed2' will be inherited from the previous version. | ||
186 | 2261 | |||
187 | 2262 | >>> class IGoodDestructorEntry(Interface): | ||
188 | 2263 | ... export_as_webservice_entry() | ||
189 | 2264 | ... | ||
190 | 2265 | ... @call_with(fixed1="value3") | ||
191 | 2266 | ... @operation_for_version('1.0') | ||
192 | 2267 | ... @export_destructor_operation() | ||
193 | 2268 | ... @call_with(fixed1="value1", fixed2="value") | ||
194 | 2269 | ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine()) | ||
195 | 2270 | ... def destructor(fixed1, fixed2): | ||
196 | 2271 | ... """Another destructor method.""" | ||
197 | 2272 | |||
198 | 2273 | >>> ignore = generate_entry_interfaces( | ||
199 | 2274 | ... IGoodDestructorEntry, 'beta', '1.0') | ||
200 | 2275 | |||
201 | 2276 | This is not fine. In this case, the destructor is removed in 1.0 and | ||
202 | 2277 | added back in 2.0. The 2.0 version does not inherit any values from | ||
203 | 2278 | its prior incarnation, so the fact that it does not fix any value for | ||
204 | 2279 | 'fixed2' is a problem. | ||
205 | 2280 | |||
206 | 2281 | >>> class IBadDestructorEntry(Interface): | ||
207 | 2282 | ... export_as_webservice_entry() | ||
208 | 2283 | ... | ||
209 | 2284 | ... @export_destructor_operation() | ||
210 | 2285 | ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine()) | ||
211 | 2286 | ... @call_with(fixed1="value3") | ||
212 | 2287 | ... @operation_for_version('2.0') | ||
213 | 2288 | ... @operation_removed_in_version('1.0') | ||
214 | 2289 | ... @export_destructor_operation() | ||
215 | 2290 | ... @call_with(fixed1="value1", fixed2="value") | ||
216 | 2291 | ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine()) | ||
217 | 2292 | ... def destructor(fixed1, fixed2): | ||
218 | 2293 | ... """Another destructor method.""" | ||
219 | 2294 | Traceback (most recent call last): | ||
220 | 2295 | ... | ||
221 | 2296 | TypeError: A destructor method must take no non-fixed | ||
222 | 2297 | arguments. In version 2.0, the "destructor" method takes 1: | ||
223 | 2298 | "fixed2". | ||
224 | 2299 | |||
225 | 2300 | |||
226 | 2250 | Security | 2301 | Security |
227 | 2251 | ======== | 2302 | ======== |
228 | 2252 | 2303 |
This branch adds multi-version error checking for destructor methods. A destructor method cannot have any arguments that are not fixed to specific values. The previous code was not version-aware, so it was only checking for the most recent version of the web service. In this branch I check every version in which the destructor method is published.
I also defined a tiny helper method, _version_name, which takes care of this logic which I was putting all over the place when printing out error messages:
if version is None:
version = "(earliest version)"