Merge lp:~leonardr/lazr.restful/561521 into lp:lazr.restful
- 561521
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Edwin Grubbs |
Approved revision: | 130 |
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/561521 |
Merge into: | lp:lazr.restful |
Diff against target: |
436 lines (+123/-47) 5 files modified
src/lazr/restful/NEWS.txt (+8/-3) src/lazr/restful/_resource.py (+46/-13) src/lazr/restful/docs/webservice-declarations.txt (+6/-13) src/lazr/restful/docs/webservice.txt (+62/-17) src/lazr/restful/version.txt (+1/-1) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/561521 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Edwin Grubbs (community) | Approve | ||
Review via email: mp+23404@code.launchpad.net |
Commit message
Description of the change
This branch fixes bug 561521, in which a Launchpad test was found to fail on some Lucid installs because modifications dictated by PATCH requests happened in a nondeterministic order. This branch enforces a 1) deterministic order that's 2) less likely to cause problems.
The new order is less likely to cause problems because all of an entry's fields that are handled by mutator methods are saved until last. For testability purposes, I defined a helper function get_entry_
I also did some drive-by cleanup of test headings.
I have one slight misgiving about the get_entry_
You could argue that I'm really looking for an instance of PropertyWithMut
Leonard Richardson (leonardr) wrote : | # |
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Leonard,
This branch looks good. I just have one comment below.
merge-conditional
-Edwin
>=== modified file 'src/lazr/
>--- src/lazr/
>+++ src/lazr/
>@@ -1988,3 +1988,36 @@
> """The URL to the description of the object's full representation."""
> return "%s#%s-full" % (
> self._service_
>+
>+
>+def get_entry_
>+ """Return the entry's fields in the order they should be written to.
>+
>+ The ordering is intended to 1) be deterministic for a given schema
>+ and 2) minimize the chance of conflicts. Fields that are just
>+ fields come before fields (believed to be) controlled by
>+ mutators. Within each group, fields are returned in the order they
>+ appear in the schema.
>+
>+ :param entry: An object that provides IEntry.
>+ :return: A list of 2-tuples (field name, field object)
>+ """
>+ non_mutator_fields = []
>+ mutator_fields = []
>+ field_implement
>+ for name, field in getFieldsInOrde
>+ if name.startswith
>+ # This field is not part of the web service interface.
>+ continue
>+
>+ add_to = non_mutator_fields
>+ # If this field is secretly a subclass of lazr.delegates
>+ # Passthrough (but not a direct instance of Passthrough), put
>+ # it at the end -- it's probably controlled by a mutator.
>+ implementation = field_implement
>+ if (issubclass(
>+ and not implementation.
>+ add_to = mutator_fields
>+ add_to.
>+ return non_mutator_fields + mutator_fields
Setting the add_to variable makes it a little harder to follow the
logic. I would prefer getting rid of it and using an if/else with
mutator_
Preview Diff
1 | === modified file 'src/lazr/restful/NEWS.txt' | |||
2 | --- src/lazr/restful/NEWS.txt 2010-04-07 15:01:33 +0000 | |||
3 | +++ src/lazr/restful/NEWS.txt 2010-04-14 15:29:18 +0000 | |||
4 | @@ -2,8 +2,8 @@ | |||
5 | 2 | NEWS for lazr.restful | 2 | NEWS for lazr.restful |
6 | 3 | ===================== | 3 | ===================== |
7 | 4 | 4 | ||
10 | 5 | Development | 5 | 0.9.25 (2010-04-14) |
11 | 6 | =========== | 6 | =================== |
12 | 7 | 7 | ||
13 | 8 | Special note: This version introduces a new configuration element, | 8 | Special note: This version introduces a new configuration element, |
14 | 9 | 'caching_policy'. This element starts out simple but may become more | 9 | 'caching_policy'. This element starts out simple but may become more |
15 | @@ -12,7 +12,12 @@ | |||
16 | 12 | 12 | ||
17 | 13 | Service root resources are now client-side cacheable for an amount of | 13 | Service root resources are now client-side cacheable for an amount of |
18 | 14 | time that depends on the server configuration and the version of the | 14 | time that depends on the server configuration and the version of the |
20 | 15 | web service requested. | 15 | web service requested. To get the full benefit, clients will need to |
21 | 16 | upgrade to lazr.restfulclient 0.9.14. | ||
22 | 17 | |||
23 | 18 | When a PATCH or PUT request changes multiple fields at once, the | ||
24 | 19 | changes are applied in a deterministic order designed to minimize | ||
25 | 20 | possible conflicts. | ||
26 | 16 | 21 | ||
27 | 17 | 0.9.24 (2010-03-17) | 22 | 0.9.24 (2010-03-17) |
28 | 18 | ==================== | 23 | ==================== |
29 | 19 | 24 | ||
30 | === modified file 'src/lazr/restful/_resource.py' | |||
31 | --- src/lazr/restful/_resource.py 2010-04-13 15:35:24 +0000 | |||
32 | +++ src/lazr/restful/_resource.py 2010-04-14 15:29:18 +0000 | |||
33 | @@ -76,6 +76,7 @@ | |||
34 | 76 | from zope.traversing.browser.interfaces import IAbsoluteURL | 76 | from zope.traversing.browser.interfaces import IAbsoluteURL |
35 | 77 | 77 | ||
36 | 78 | from lazr.batchnavigator import BatchNavigator | 78 | from lazr.batchnavigator import BatchNavigator |
37 | 79 | from lazr.delegates import Passthrough | ||
38 | 79 | from lazr.enum import BaseItem | 80 | from lazr.enum import BaseItem |
39 | 80 | from lazr.lifecycle.event import ObjectModifiedEvent | 81 | from lazr.lifecycle.event import ObjectModifiedEvent |
40 | 81 | from lazr.lifecycle.snapshot import Snapshot | 82 | from lazr.lifecycle.snapshot import Snapshot |
41 | @@ -956,7 +957,7 @@ | |||
42 | 956 | propagated to the client. | 957 | propagated to the client. |
43 | 957 | """ | 958 | """ |
44 | 958 | changeset = copy.copy(changeset) | 959 | changeset = copy.copy(changeset) |
46 | 959 | validated_changeset = {} | 960 | validated_changeset = [] |
47 | 960 | errors = [] | 961 | errors = [] |
48 | 961 | 962 | ||
49 | 962 | # The self link and resource type link aren't part of the | 963 | # The self link and resource type link aren't part of the |
50 | @@ -983,12 +984,7 @@ | |||
51 | 983 | 984 | ||
52 | 984 | # For every field in the schema, see if there's a corresponding | 985 | # For every field in the schema, see if there's a corresponding |
53 | 985 | # field in the changeset. | 986 | # field in the changeset. |
60 | 986 | # Get the fields ordered by name so that we always evaluate them in | 987 | for name, field in get_entry_fields_in_write_order(self.entry): |
55 | 987 | # the same order. This is needed to predict errors when testing. | ||
56 | 988 | for name, field in getFieldsInOrder(self.entry.schema): | ||
57 | 989 | if name.startswith('_'): | ||
58 | 990 | # This field is not part of the web service interface. | ||
59 | 991 | continue | ||
61 | 992 | field = field.bind(self.entry.context) | 988 | field = field.bind(self.entry.context) |
62 | 993 | marshaller = getMultiAdapter((field, self.request), | 989 | marshaller = getMultiAdapter((field, self.request), |
63 | 994 | IFieldMarshaller) | 990 | IFieldMarshaller) |
64 | @@ -1114,7 +1110,7 @@ | |||
65 | 1114 | error = "Validation error" | 1110 | error = "Validation error" |
66 | 1115 | errors.append("%s: %s" % (repr_name, error)) | 1111 | errors.append("%s: %s" % (repr_name, error)) |
67 | 1116 | continue | 1112 | continue |
69 | 1117 | validated_changeset[field] = (name, value) | 1113 | validated_changeset.append((field, value)) |
70 | 1118 | # If there are any fields left in the changeset, they're | 1114 | # If there are any fields left in the changeset, they're |
71 | 1119 | # fields that don't correspond to some field in the | 1115 | # fields that don't correspond to some field in the |
72 | 1120 | # schema. They're all errors. | 1116 | # schema. They're all errors. |
73 | @@ -1134,8 +1130,10 @@ | |||
74 | 1134 | 1130 | ||
75 | 1135 | # Store the entry's current URL so we can see if it changes. | 1131 | # Store the entry's current URL so we can see if it changes. |
76 | 1136 | original_url = absoluteURL(self.entry.context, self.request) | 1132 | original_url = absoluteURL(self.entry.context, self.request) |
79 | 1137 | # Make the changes. | 1133 | |
80 | 1138 | for field, (name, value) in validated_changeset.items(): | 1134 | # Make the changes, in the same order obtained from |
81 | 1135 | # get_entry_fields_in_write_order. | ||
82 | 1136 | for field, value in validated_changeset: | ||
83 | 1139 | field.set(self.entry, value) | 1137 | field.set(self.entry, value) |
84 | 1140 | # The representation has changed, and etags will need to be | 1138 | # The representation has changed, and etags will need to be |
85 | 1141 | # recalculated. | 1139 | # recalculated. |
86 | @@ -1145,7 +1143,7 @@ | |||
87 | 1145 | event = ObjectModifiedEvent( | 1143 | event = ObjectModifiedEvent( |
88 | 1146 | object=self.entry.context, | 1144 | object=self.entry.context, |
89 | 1147 | object_before_modification=entry_before_modification, | 1145 | object_before_modification=entry_before_modification, |
91 | 1148 | edited_fields=validated_changeset.keys()) | 1146 | edited_fields=[field for field, value in validated_changeset]) |
92 | 1149 | notify(event) | 1147 | notify(event) |
93 | 1150 | 1148 | ||
94 | 1151 | # The changeset contained new values for some of this object's | 1149 | # The changeset contained new values for some of this object's |
95 | @@ -1430,8 +1428,10 @@ | |||
96 | 1430 | 1428 | ||
97 | 1431 | # Make sure the representation includes values for all | 1429 | # Make sure the representation includes values for all |
98 | 1432 | # writable attributes. | 1430 | # writable attributes. |
101 | 1433 | # Get the fields ordered by name so that we always evaluate them in | 1431 | # |
102 | 1434 | # the same order. This is needed to predict errors when testing. | 1432 | # Get the fields ordered by schema order so that we always |
103 | 1433 | # evaluate them in the same order. This is needed to predict | ||
104 | 1434 | # errors when testing. | ||
105 | 1435 | for name, field in getFieldsInOrder(self.entry.schema): | 1435 | for name, field in getFieldsInOrder(self.entry.schema): |
106 | 1436 | if not self.isModifiableField(field, True): | 1436 | if not self.isModifiableField(field, True): |
107 | 1437 | continue | 1437 | continue |
108 | @@ -1988,3 +1988,36 @@ | |||
109 | 1988 | """The URL to the description of the object's full representation.""" | 1988 | """The URL to the description of the object's full representation.""" |
110 | 1989 | return "%s#%s-full" % ( | 1989 | return "%s#%s-full" % ( |
111 | 1990 | self._service_root_url(), self.singular_type) | 1990 | self._service_root_url(), self.singular_type) |
112 | 1991 | |||
113 | 1992 | |||
114 | 1993 | def get_entry_fields_in_write_order(entry): | ||
115 | 1994 | """Return the entry's fields in the order they should be written to. | ||
116 | 1995 | |||
117 | 1996 | The ordering is intended to 1) be deterministic for a given schema | ||
118 | 1997 | and 2) minimize the chance of conflicts. Fields that are just | ||
119 | 1998 | fields come before fields (believed to be) controlled by | ||
120 | 1999 | mutators. Within each group, fields are returned in the order they | ||
121 | 2000 | appear in the schema. | ||
122 | 2001 | |||
123 | 2002 | :param entry: An object that provides IEntry. | ||
124 | 2003 | :return: A list of 2-tuples (field name, field object) | ||
125 | 2004 | """ | ||
126 | 2005 | non_mutator_fields = [] | ||
127 | 2006 | mutator_fields = [] | ||
128 | 2007 | field_implementations = entry.__class__.__dict__ | ||
129 | 2008 | for name, field in getFieldsInOrder(entry.schema): | ||
130 | 2009 | if name.startswith('_'): | ||
131 | 2010 | # This field is not part of the web service interface. | ||
132 | 2011 | continue | ||
133 | 2012 | |||
134 | 2013 | add_to = non_mutator_fields | ||
135 | 2014 | # If this field is secretly a subclass of lazr.delegates | ||
136 | 2015 | # Passthrough (but not a direct instance of Passthrough), put | ||
137 | 2016 | # it at the end -- it's probably controlled by a mutator. | ||
138 | 2017 | implementation = field_implementations[name] | ||
139 | 2018 | if (issubclass(implementation.__class__, Passthrough) | ||
140 | 2019 | and not implementation.__class__ is Passthrough): | ||
141 | 2020 | add_to = mutator_fields | ||
142 | 2021 | add_to.append((name, field)) | ||
143 | 2022 | return non_mutator_fields + mutator_fields | ||
144 | 2023 | |||
145 | 1991 | 2024 | ||
146 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' | |||
147 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-03-03 13:38:05 +0000 | |||
148 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-04-14 15:29:18 +0000 | |||
149 | @@ -5,7 +5,6 @@ | |||
150 | 5 | with some decorators. From this tagging the web service API will be | 5 | with some decorators. From this tagging the web service API will be |
151 | 6 | created automatically. | 6 | created automatically. |
152 | 7 | 7 | ||
153 | 8 | ======================== | ||
154 | 9 | Exporting the data model | 8 | Exporting the data model |
155 | 10 | ======================== | 9 | ======================== |
156 | 11 | 10 | ||
157 | @@ -770,13 +769,11 @@ | |||
158 | 770 | return_type: None | 769 | return_type: None |
159 | 771 | type: 'write_operation' | 770 | type: 'write_operation' |
160 | 772 | 771 | ||
161 | 773 | ========================= | ||
162 | 774 | Generating the webservice | 772 | Generating the webservice |
163 | 775 | ========================= | 773 | ========================= |
164 | 776 | 774 | ||
165 | 777 | |||
166 | 778 | Entry | 775 | Entry |
168 | 779 | ===== | 776 | ----- |
169 | 780 | 777 | ||
170 | 781 | The webservice can be generated from tagged interfaces. For every | 778 | The webservice can be generated from tagged interfaces. For every |
171 | 782 | version in the web service, generate_entry_interfaces() will create a | 779 | version in the web service, generate_entry_interfaces() will create a |
172 | @@ -969,9 +966,8 @@ | |||
173 | 969 | ... | 966 | ... |
174 | 970 | TypeError: 'IBookSet' isn't exported as an entry. | 967 | TypeError: 'IBookSet' isn't exported as an entry. |
175 | 971 | 968 | ||
176 | 972 | |||
177 | 973 | Collection | 969 | Collection |
179 | 974 | ========== | 970 | ---------- |
180 | 975 | 971 | ||
181 | 976 | An ICollection adapter for content interface tagged as being exported as | 972 | An ICollection adapter for content interface tagged as being exported as |
182 | 977 | collections on the webservice can be generated by using the | 973 | collections on the webservice can be generated by using the |
183 | @@ -1046,7 +1042,7 @@ | |||
184 | 1046 | TypeError: 'IBook' isn't exported as a collection. | 1042 | TypeError: 'IBook' isn't exported as a collection. |
185 | 1047 | 1043 | ||
186 | 1048 | Methods | 1044 | Methods |
188 | 1049 | ======= | 1045 | ------- |
189 | 1050 | 1046 | ||
190 | 1051 | IResourceOperation adapters can be generated for exported methods by | 1047 | IResourceOperation adapters can be generated for exported methods by |
191 | 1052 | using the generate_operation_adapter() function. Using it on a method | 1048 | using the generate_operation_adapter() function. Using it on a method |
192 | @@ -1259,10 +1255,8 @@ | |||
193 | 1259 | >>> print destructor_method_adapter_factory.__name__ | 1255 | >>> print destructor_method_adapter_factory.__name__ |
194 | 1260 | DELETE_IBookOnSteroids_destroy_beta | 1256 | DELETE_IBookOnSteroids_destroy_beta |
195 | 1261 | 1257 | ||
196 | 1262 | |||
197 | 1263 | |||
198 | 1264 | Destructor | 1258 | Destructor |
200 | 1265 | ========== | 1259 | ---------- |
201 | 1266 | 1260 | ||
202 | 1267 | A method can be designated as a destructor for the entry. Here, the | 1261 | A method can be designated as a destructor for the entry. Here, the |
203 | 1268 | destroy() method is designated as the destructor for IHasText. | 1262 | destroy() method is designated as the destructor for IHasText. |
204 | @@ -1322,9 +1316,8 @@ | |||
205 | 1322 | TypeError: An entry can only have one destructor method for | 1316 | TypeError: An entry can only have one destructor method for |
206 | 1323 | version (earliest version); destroy and destroy2 make two. | 1317 | version (earliest version); destroy and destroy2 make two. |
207 | 1324 | 1318 | ||
208 | 1325 | |||
209 | 1326 | Mutators | 1319 | Mutators |
211 | 1327 | ======== | 1320 | -------- |
212 | 1328 | 1321 | ||
213 | 1329 | A method can be designated as a mutator for some field. Here, the | 1322 | A method can be designated as a mutator for some field. Here, the |
214 | 1330 | set_text() method is designated as the mutator for the 'text' field. | 1323 | set_text() method is designated as the mutator for the 'text' field. |
215 | @@ -1471,7 +1464,7 @@ | |||
216 | 1471 | (earliest version); set_value_2 makes two. | 1464 | (earliest version); set_value_2 makes two. |
217 | 1472 | 1465 | ||
218 | 1473 | Caching | 1466 | Caching |
220 | 1474 | ======= | 1467 | ------- |
221 | 1475 | 1468 | ||
222 | 1476 | It is possible to cache a server response in the browser cache using | 1469 | It is possible to cache a server response in the browser cache using |
223 | 1477 | the @cache_for decorator: | 1470 | the @cache_for decorator: |
224 | 1478 | 1471 | ||
225 | === modified file 'src/lazr/restful/docs/webservice.txt' | |||
226 | --- src/lazr/restful/docs/webservice.txt 2010-02-11 17:57:16 +0000 | |||
227 | +++ src/lazr/restful/docs/webservice.txt 2010-04-14 15:29:18 +0000 | |||
228 | @@ -1,3 +1,4 @@ | |||
229 | 1 | ******************** | ||
230 | 1 | RESTful Web Services | 2 | RESTful Web Services |
231 | 2 | ******************** | 3 | ******************** |
232 | 3 | 4 | ||
233 | @@ -6,7 +7,6 @@ | |||
234 | 6 | a model for managing recipes, and then publishing the model objects as | 7 | a model for managing recipes, and then publishing the model objects as |
235 | 7 | resources through a web service. | 8 | resources through a web service. |
236 | 8 | 9 | ||
237 | 9 | ===================== | ||
238 | 10 | Example model objects | 10 | Example model objects |
239 | 11 | ===================== | 11 | ===================== |
240 | 12 | 12 | ||
241 | @@ -473,7 +473,6 @@ | |||
242 | 473 | >>> setSecurityPolicy(SimpleSecurityPolicy) | 473 | >>> setSecurityPolicy(SimpleSecurityPolicy) |
243 | 474 | <class ...> | 474 | <class ...> |
244 | 475 | 475 | ||
245 | 476 | ========================================= | ||
246 | 477 | Web Service Infrastructure Initialization | 476 | Web Service Infrastructure Initialization |
247 | 478 | ========================================= | 477 | ========================================= |
248 | 479 | 478 | ||
249 | @@ -532,7 +531,6 @@ | |||
250 | 532 | ... register_versioned_request_utility(cls, version) | 531 | ... register_versioned_request_utility(cls, version) |
251 | 533 | 532 | ||
252 | 534 | 533 | ||
253 | 535 | ====================== | ||
254 | 536 | Defining the resources | 534 | Defining the resources |
255 | 537 | ====================== | 535 | ====================== |
256 | 538 | 536 | ||
257 | @@ -624,7 +622,6 @@ | |||
258 | 624 | ... [IChoice, IWebServiceClientRequest, AuthorVocabulary], | 622 | ... [IChoice, IWebServiceClientRequest, AuthorVocabulary], |
259 | 625 | ... IFieldMarshaller) | 623 | ... IFieldMarshaller) |
260 | 626 | 624 | ||
261 | 627 | ========================== | ||
262 | 628 | Implementing the resources | 625 | Implementing the resources |
263 | 629 | ========================== | 626 | ========================== |
264 | 630 | 627 | ||
265 | @@ -806,7 +803,67 @@ | |||
266 | 806 | >>> scoped_collection.entry_schema | 803 | >>> scoped_collection.entry_schema |
267 | 807 | <InterfaceClass __builtin__.IRecipeEntry> | 804 | <InterfaceClass __builtin__.IRecipeEntry> |
268 | 808 | 805 | ||
270 | 809 | ================= | 806 | Field ordering |
271 | 807 | -------------- | ||
272 | 808 | |||
273 | 809 | When an entry's fields are modified, it's important that the | ||
274 | 810 | modifications happen in a deterministic order, to minimize (or at | ||
275 | 811 | least make deterministic) bad interactions between fields. The helper | ||
276 | 812 | function get_entry_fields_in_write_order() handles this. | ||
277 | 813 | |||
278 | 814 | Ordinarily, fields are written to in the same order they are found in | ||
279 | 815 | the underlying schema. | ||
280 | 816 | |||
281 | 817 | >>> author_entry = getMultiAdapter((A1, request), IEntry) | ||
282 | 818 | >>> from lazr.restful._resource import get_entry_fields_in_write_order | ||
283 | 819 | >>> def print_fields_in_write_order(entry): | ||
284 | 820 | ... for name, field in get_entry_fields_in_write_order(entry): | ||
285 | 821 | ... print name | ||
286 | 822 | |||
287 | 823 | >>> print_fields_in_write_order(author_entry) | ||
288 | 824 | name | ||
289 | 825 | favorite_recipe | ||
290 | 826 | popularity | ||
291 | 827 | |||
292 | 828 | The one exception is if a field is wrapped in a subclass of the | ||
293 | 829 | Passthrough class defined by the lazr.delegates library. Classes | ||
294 | 830 | generated through lazr.restful's annotations use a Passthrough | ||
295 | 831 | subclass to control a field that triggers complex logic when its value | ||
296 | 832 | changes. To minimize the risk of bad interactions, all the simple | ||
297 | 833 | fields are changed before any of the complex fields. | ||
298 | 834 | |||
299 | 835 | Here's a simple subclass of Passthrough. | ||
300 | 836 | |||
301 | 837 | >>> from lazr.delegates import Passthrough | ||
302 | 838 | >>> class MyPassthrough(Passthrough): | ||
303 | 839 | ... pass | ||
304 | 840 | |||
305 | 841 | When we replace 'favorite_recipe' with an instance of this subclass, | ||
306 | 842 | that field shows up at the end of the list of fields. | ||
307 | 843 | |||
308 | 844 | >>> old_favorite_recipe = AuthorEntry.favorite_recipe | ||
309 | 845 | >>> AuthorEntry.favorite_recipe = MyPassthrough('favorite_recipe', A1) | ||
310 | 846 | >>> print_fields_in_write_order(author_entry) | ||
311 | 847 | name | ||
312 | 848 | popularity | ||
313 | 849 | favorite_recipe | ||
314 | 850 | |||
315 | 851 | When we replace 'name' with a Passthrough subclass, it also shows up | ||
316 | 852 | at the end--but it still shows up before 'favorite_recipe', because it | ||
317 | 853 | comes before 'favorite_recipe' in the schema. | ||
318 | 854 | |||
319 | 855 | >>> old_name = AuthorEntry.name | ||
320 | 856 | >>> AuthorEntry.name = MyPassthrough('name', A1) | ||
321 | 857 | >>> print_fields_in_write_order(author_entry) | ||
322 | 858 | popularity | ||
323 | 859 | name | ||
324 | 860 | favorite_recipe | ||
325 | 861 | |||
326 | 862 | Cleanup to restore the old AuthorEntry implementation: | ||
327 | 863 | |||
328 | 864 | >>> AuthorEntry.favorite_recipe = old_favorite_recipe | ||
329 | 865 | >>> AuthorEntry.name = old_name | ||
330 | 866 | |||
331 | 810 | Custom operations | 867 | Custom operations |
332 | 811 | ================= | 868 | ================= |
333 | 812 | 869 | ||
334 | @@ -923,7 +980,6 @@ | |||
335 | 923 | ... RecipeDeleteOperation, name="") | 980 | ... RecipeDeleteOperation, name="") |
336 | 924 | 981 | ||
337 | 925 | 982 | ||
338 | 926 | ================ | ||
339 | 927 | Resource objects | 983 | Resource objects |
340 | 928 | ================ | 984 | ================ |
341 | 929 | 985 | ||
342 | @@ -944,7 +1000,6 @@ | |||
343 | 944 | Similarly, you can implement ``RecipeEntry`` to the ``IEntry`` interface, and | 1000 | Similarly, you can implement ``RecipeEntry`` to the ``IEntry`` interface, and |
344 | 945 | expose it through the web as an ``EntryResource``. | 1001 | expose it through the web as an ``EntryResource``. |
345 | 946 | 1002 | ||
346 | 947 | ========================= | ||
347 | 948 | The Service Root Resource | 1003 | The Service Root Resource |
348 | 949 | ========================= | 1004 | ========================= |
349 | 950 | 1005 | ||
350 | @@ -1046,7 +1101,6 @@ | |||
351 | 1046 | AssertionError: There must be one (and only one) adapter | 1101 | AssertionError: There must be one (and only one) adapter |
352 | 1047 | from DishCollection to ICollection. | 1102 | from DishCollection to ICollection. |
353 | 1048 | 1103 | ||
354 | 1049 | ==================== | ||
355 | 1050 | Collection resources | 1104 | Collection resources |
356 | 1051 | ==================== | 1105 | ==================== |
357 | 1052 | 1106 | ||
358 | @@ -1234,7 +1288,6 @@ | |||
359 | 1234 | u'Nouvelle Brazilian' | 1288 | u'Nouvelle Brazilian' |
360 | 1235 | 1289 | ||
361 | 1236 | 1290 | ||
362 | 1237 | =============== | ||
363 | 1238 | Entry resources | 1291 | Entry resources |
364 | 1239 | =============== | 1292 | =============== |
365 | 1240 | 1293 | ||
366 | @@ -1381,7 +1434,6 @@ | |||
367 | 1381 | u'Draw, singe, stuff, and truss...', | 1434 | u'Draw, singe, stuff, and truss...', |
368 | 1382 | u'You can always judge...'] | 1435 | u'You can always judge...'] |
369 | 1383 | 1436 | ||
370 | 1384 | ============================= | ||
371 | 1385 | Named operation return values | 1437 | Named operation return values |
372 | 1386 | ============================= | 1438 | ============================= |
373 | 1387 | 1439 | ||
374 | @@ -1514,7 +1566,6 @@ | |||
375 | 1514 | ... | 1566 | ... |
376 | 1515 | TypeError: Could not serialize object [<object object...>] to JSON. | 1567 | TypeError: Could not serialize object [<object object...>] to JSON. |
377 | 1516 | 1568 | ||
378 | 1517 | ===== | ||
379 | 1518 | ETags | 1569 | ETags |
380 | 1519 | ===== | 1570 | ===== |
381 | 1520 | 1571 | ||
382 | @@ -1580,7 +1631,6 @@ | |||
383 | 1580 | >>> etag_after_readonly_change == etag_original | 1631 | >>> etag_after_readonly_change == etag_original |
384 | 1581 | False | 1632 | False |
385 | 1582 | 1633 | ||
386 | 1583 | =================== | ||
387 | 1584 | Resource Visibility | 1634 | Resource Visibility |
388 | 1585 | =================== | 1635 | =================== |
389 | 1586 | 1636 | ||
390 | @@ -1684,7 +1734,6 @@ | |||
391 | 1684 | ... | 1734 | ... |
392 | 1685 | Unauthorized: (<Recipe object...>, 'dish', ...) | 1735 | Unauthorized: (<Recipe object...>, 'dish', ...) |
393 | 1686 | 1736 | ||
394 | 1687 | ===================== | ||
395 | 1688 | Stored file resources | 1737 | Stored file resources |
396 | 1689 | ===================== | 1738 | ===================== |
397 | 1690 | 1739 | ||
398 | @@ -1753,7 +1802,6 @@ | |||
399 | 1753 | >>> print C2.cover | 1802 | >>> print C2.cover |
400 | 1754 | None | 1803 | None |
401 | 1755 | 1804 | ||
402 | 1756 | =============== | ||
403 | 1757 | Field resources | 1805 | Field resources |
404 | 1758 | =============== | 1806 | =============== |
405 | 1759 | 1807 | ||
406 | @@ -1764,7 +1812,6 @@ | |||
407 | 1764 | >>> print field_resource() | 1812 | >>> print field_resource() |
408 | 1765 | "The Joy of Cooking" | 1813 | "The Joy of Cooking" |
409 | 1766 | 1814 | ||
410 | 1767 | ================================== | ||
411 | 1768 | Requesting non available resources | 1815 | Requesting non available resources |
412 | 1769 | ================================== | 1816 | ================================== |
413 | 1770 | 1817 | ||
414 | @@ -1793,7 +1840,6 @@ | |||
415 | 1793 | ... | 1840 | ... |
416 | 1794 | NotFound: ... name: u'comments/10' | 1841 | NotFound: ... name: u'comments/10' |
417 | 1795 | 1842 | ||
418 | 1796 | ==================== | ||
419 | 1797 | Manipulating entries | 1843 | Manipulating entries |
420 | 1798 | ==================== | 1844 | ==================== |
421 | 1799 | 1845 | ||
422 | @@ -1965,7 +2011,6 @@ | |||
423 | 1965 | NotFound: ... name: u'recipes/Foies de voilaille en aspic' | 2011 | NotFound: ... name: u'recipes/Foies de voilaille en aspic' |
424 | 1966 | 2012 | ||
425 | 1967 | 2013 | ||
426 | 1968 | ================= | ||
427 | 1969 | Within a template | 2014 | Within a template |
428 | 1970 | ================= | 2015 | ================= |
429 | 1971 | 2016 | ||
430 | 1972 | 2017 | ||
431 | === modified file 'src/lazr/restful/version.txt' | |||
432 | --- src/lazr/restful/version.txt 2010-03-15 19:44:45 +0000 | |||
433 | +++ src/lazr/restful/version.txt 2010-04-14 15:29:18 +0000 | |||
434 | @@ -1,1 +1,1 @@ | |||
436 | 1 | 0.9.24 | 1 | 0.9.25 |
Oh, I should mention that in addition to testing the unit tests, I tested this branch in conjunction with Launchpad to make sure it made the test failure go away. (The test that failed was webservice/ xx-distribution .txt)