Merge lp:~leonardr/lazr.restful/operation-removed-in into lp:lazr.restful
- operation-removed-in
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Approved by: | Gary Poster |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/operation-removed-in |
Merge into: | lp:lazr.restful |
Diff against target: |
355 lines (+226/-84) 2 files modified
src/lazr/restful/declarations.py (+19/-4) src/lazr/restful/docs/webservice-declarations.txt (+207/-80) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/operation-removed-in |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster | Approve | ||
Review via email: mp+17784@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
Gary Poster (gary) wrote : | # |
Approved (with typo fix for "versuibed").
Thank you
Gary
review:
Approve
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-01-20 15:21:38 +0000 | |||
3 | +++ src/lazr/restful/declarations.py 2010-01-20 21:42:13 +0000 | |||
4 | @@ -299,7 +299,7 @@ | |||
5 | 299 | # in the interface method specification. | 299 | # in the interface method specification. |
6 | 300 | annotations = method.__dict__.get(LAZR_WEBSERVICE_EXPORTED, None) | 300 | annotations = method.__dict__.get(LAZR_WEBSERVICE_EXPORTED, None) |
7 | 301 | if annotations is None: | 301 | if annotations is None: |
9 | 302 | # Create a new bleed-through dict which associates | 302 | # Create a new versioned dict which associates |
10 | 303 | # annotation data with the earliest active version of the | 303 | # annotation data with the earliest active version of the |
11 | 304 | # web service. Future @webservice_version annotations will | 304 | # web service. Future @webservice_version annotations will |
12 | 305 | # push later versions onto the VersionedDict, allowing | 305 | # push later versions onto the VersionedDict, allowing |
13 | @@ -475,12 +475,27 @@ | |||
14 | 475 | def annotate_method(self, method, annotations): | 475 | def annotate_method(self, method, annotations): |
15 | 476 | """See `_method_annotator`.""" | 476 | """See `_method_annotator`.""" |
16 | 477 | # The annotations dict is a VersionedDict. Push a new dict | 477 | # The annotations dict is a VersionedDict. Push a new dict |
20 | 478 | # onto its stack, labeled with the version number, so that | 478 | # onto its stack, labeled with the version number, and copy in |
21 | 479 | # future annotations can override old annotations without | 479 | # the old version's annotations so that this version can |
22 | 480 | # destroying them. | 480 | # modify those annotations without destroying them. |
23 | 481 | annotations.push(self.version) | 481 | annotations.push(self.version) |
24 | 482 | 482 | ||
25 | 483 | 483 | ||
26 | 484 | class operation_removed_in_version(operation_for_version): | ||
27 | 485 | """Decoration removing this operation from the web service. | ||
28 | 486 | |||
29 | 487 | This operation will not be present in the given version of the web | ||
30 | 488 | service, or any subsequent version, unless it's re-published with | ||
31 | 489 | an export_*_operation method. | ||
32 | 490 | """ | ||
33 | 491 | def annotate_method(self, method, annotations): | ||
34 | 492 | """See `_method_annotator`.""" | ||
35 | 493 | # The annotations dict is a VersionedDict. Push a new dict | ||
36 | 494 | # onto its stack, labeled with the version number. Make sure the | ||
37 | 495 | # new dict is empty rather than copying the old annotations | ||
38 | 496 | annotations.push(self.version, True) | ||
39 | 497 | |||
40 | 498 | |||
41 | 484 | class export_operation_as(_method_annotator): | 499 | class export_operation_as(_method_annotator): |
42 | 485 | """Decorator specifying the name to export the method as.""" | 500 | """Decorator specifying the name to export the method as.""" |
43 | 486 | 501 | ||
44 | 487 | 502 | ||
45 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' | |||
46 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-01-20 13:43:44 +0000 | |||
47 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-20 21:42:13 +0000 | |||
48 | @@ -503,85 +503,6 @@ | |||
49 | 503 | >>> param_defs['optional2'].default | 503 | >>> param_defs['optional2'].default |
50 | 504 | u'Default2' | 504 | u'Default2' |
51 | 505 | 505 | ||
52 | 506 | Versioning | ||
53 | 507 | ---------- | ||
54 | 508 | |||
55 | 509 | Different versions of the webservice can publish the same interface | ||
56 | 510 | method in totally different ways. Here's a simple example. This method | ||
57 | 511 | appears differently in three versions of the web service: 2.0, 1.0, | ||
58 | 512 | and in an unnamed pre-1.0 version. | ||
59 | 513 | |||
60 | 514 | >>> from lazr.restful.declarations import operation_for_version | ||
61 | 515 | >>> class MultiVersionMethod(Interface): | ||
62 | 516 | ... export_as_webservice_entry() | ||
63 | 517 | ... | ||
64 | 518 | ... @call_with(fixed='2.0 value') | ||
65 | 519 | ... @operation_for_version('2.0') | ||
66 | 520 | ... | ||
67 | 521 | ... @call_with(fixed='1.0 value') | ||
68 | 522 | ... @export_operation_as('new_name') | ||
69 | 523 | ... @rename_parameters_as(required="required_argument") | ||
70 | 524 | ... @operation_for_version('1.0') | ||
71 | 525 | ... | ||
72 | 526 | ... @call_with(fixed='pre-1.0 value') | ||
73 | 527 | ... @operation_parameters( | ||
74 | 528 | ... required=TextLine(), | ||
75 | 529 | ... fixed=TextLine() | ||
76 | 530 | ... ) | ||
77 | 531 | ... @export_read_operation() | ||
78 | 532 | ... def a_method(required, fixed='Fixed value'): | ||
79 | 533 | ... """Method demonstrating multiversion publication.""" | ||
80 | 534 | |||
81 | 535 | The tagged value containing the annotations looks like a dictionary, | ||
82 | 536 | but it's actually a stack of dictionaries named after the versions. | ||
83 | 537 | |||
84 | 538 | >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue( | ||
85 | 539 | ... 'lazr.restful.exported') | ||
86 | 540 | >>> dictionary.dict_names | ||
87 | 541 | [None, '1.0', '2.0'] | ||
88 | 542 | |||
89 | 543 | The dictionary on top of the stack is for the 2.0 version of the web | ||
90 | 544 | service. In 2.0, the method is published as 'new_name' and its 'fixed' | ||
91 | 545 | argument is fixed to the string '2.0 value'. | ||
92 | 546 | |||
93 | 547 | >>> print dictionary['as'] | ||
94 | 548 | new_name | ||
95 | 549 | >>> dictionary['call_with'] | ||
96 | 550 | {'fixed': '2.0 value'} | ||
97 | 551 | |||
98 | 552 | The published name of the 'required' argument is 'required_argument', | ||
99 | 553 | not 'required'. | ||
100 | 554 | |||
101 | 555 | >>> print dictionary['params']['required'].__name__ | ||
102 | 556 | required_argument | ||
103 | 557 | |||
104 | 558 | Let's pop the 2.0 version off the stack. Now we can see how the method | ||
105 | 559 | looks in 1.0. It's still called 'new_name', and its 'required' | ||
106 | 560 | argument is still called 'required_argument', but its 'fixed' argument | ||
107 | 561 | is fixed to the string '1.0 value'. | ||
108 | 562 | |||
109 | 563 | >>> ignored = dictionary.pop() | ||
110 | 564 | >>> print dictionary['as'] | ||
111 | 565 | new_name | ||
112 | 566 | >>> dictionary['call_with'] | ||
113 | 567 | {'fixed': '1.0 value'} | ||
114 | 568 | >>> print dictionary['params']['required'].__name__ | ||
115 | 569 | required_argument | ||
116 | 570 | |||
117 | 571 | Let's pop one more time to see how the method looks in the pre-1.0 | ||
118 | 572 | version. It hasn't yet been renamed to 'new_name', its 'required' | ||
119 | 573 | argument hasn't yet been renamed to 'required_argument', and its | ||
120 | 574 | 'fixed' argument is fixed to the string 'pre-1.0 value'. | ||
121 | 575 | |||
122 | 576 | >>> ignored = dictionary.pop() | ||
123 | 577 | >>> print dictionary.get('as') | ||
124 | 578 | None | ||
125 | 579 | >>> print dictionary['params']['required'].__name__ | ||
126 | 580 | required | ||
127 | 581 | >>> dictionary['call_with'] | ||
128 | 582 | {'fixed': 'pre-1.0 value'} | ||
129 | 583 | |||
130 | 584 | |||
131 | 585 | Error handling | 506 | Error handling |
132 | 586 | -------------- | 507 | -------------- |
133 | 587 | 508 | ||
134 | @@ -1526,8 +1447,8 @@ | |||
135 | 1526 | It is possible to cache a server response in the browser cache using | 1447 | It is possible to cache a server response in the browser cache using |
136 | 1527 | the @cache_for decorator: | 1448 | the @cache_for decorator: |
137 | 1528 | 1449 | ||
138 | 1450 | >>> from lazr.restful.testing.webservice import FakeRequest | ||
139 | 1529 | >>> from lazr.restful.declarations import cache_for | 1451 | >>> from lazr.restful.declarations import cache_for |
140 | 1530 | >>> from lazr.restful.testing.webservice import FakeRequest | ||
141 | 1531 | >>> | 1452 | >>> |
142 | 1532 | >>> class ICachedBookSet(IBookSet): | 1453 | >>> class ICachedBookSet(IBookSet): |
143 | 1533 | ... """IBookSet supporting caching.""" | 1454 | ... """IBookSet supporting caching.""" |
144 | @@ -1577,6 +1498,212 @@ | |||
145 | 1577 | ... | 1498 | ... |
146 | 1578 | ValueError: Caching duration should be a positive number: -15 | 1499 | ValueError: Caching duration should be a positive number: -15 |
147 | 1579 | 1500 | ||
148 | 1501 | Versioned services | ||
149 | 1502 | ================== | ||
150 | 1503 | |||
151 | 1504 | Different versions of the webservice can publish the same data model | ||
152 | 1505 | object in totally different ways. | ||
153 | 1506 | |||
154 | 1507 | Named operations | ||
155 | 1508 | ---------------- | ||
156 | 1509 | |||
157 | 1510 | It's easy to reflect the most common changes between versions: | ||
158 | 1511 | operations and arguments being renamed, changes in fixed values, etc. | ||
159 | 1512 | This method appears differently in three versions of the web service: | ||
160 | 1513 | 2.0, 1.0, and in an unnamed pre-1.0 version. | ||
161 | 1514 | |||
162 | 1515 | >>> from lazr.restful.declarations import operation_for_version | ||
163 | 1516 | >>> class MultiVersionMethod(Interface): | ||
164 | 1517 | ... export_as_webservice_entry() | ||
165 | 1518 | ... | ||
166 | 1519 | ... @call_with(fixed='2.0 value') | ||
167 | 1520 | ... @cache_for(300) | ||
168 | 1521 | ... @operation_for_version('2.0') | ||
169 | 1522 | ... | ||
170 | 1523 | ... @call_with(fixed='1.0 value') | ||
171 | 1524 | ... @export_operation_as('new_name') | ||
172 | 1525 | ... @rename_parameters_as(required="required_argument") | ||
173 | 1526 | ... @operation_for_version('1.0') | ||
174 | 1527 | ... | ||
175 | 1528 | ... @call_with(fixed='pre-1.0 value') | ||
176 | 1529 | ... @cache_for(100) | ||
177 | 1530 | ... @operation_parameters( | ||
178 | 1531 | ... required=TextLine(), | ||
179 | 1532 | ... fixed=TextLine() | ||
180 | 1533 | ... ) | ||
181 | 1534 | ... @export_read_operation() | ||
182 | 1535 | ... def a_method(required, fixed='Fixed value'): | ||
183 | 1536 | ... """Method demonstrating multiversion publication.""" | ||
184 | 1537 | |||
185 | 1538 | The tagged value containing the annotations looks like a dictionary, | ||
186 | 1539 | but it's actually a stack of dictionaries named after the versions. | ||
187 | 1540 | |||
188 | 1541 | >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue( | ||
189 | 1542 | ... 'lazr.restful.exported') | ||
190 | 1543 | >>> dictionary.dict_names | ||
191 | 1544 | [None, '1.0', '2.0'] | ||
192 | 1545 | |||
193 | 1546 | The dictionary on top of the stack is for the 2.0 version of the web | ||
194 | 1547 | service. In 2.0, the method is published as 'new_name' and its 'fixed' | ||
195 | 1548 | argument is fixed to the string '2.0 value'. | ||
196 | 1549 | |||
197 | 1550 | >>> print dictionary['as'] | ||
198 | 1551 | new_name | ||
199 | 1552 | >>> dictionary['call_with'] | ||
200 | 1553 | {'fixed': '2.0 value'} | ||
201 | 1554 | >>> dictionary['cache_for'] | ||
202 | 1555 | 300 | ||
203 | 1556 | |||
204 | 1557 | The published name of the 'required' argument is 'required_argument', | ||
205 | 1558 | not 'required'. | ||
206 | 1559 | |||
207 | 1560 | >>> print dictionary['params']['required'].__name__ | ||
208 | 1561 | required_argument | ||
209 | 1562 | |||
210 | 1563 | Let's pop the 2.0 version off the stack. Now we can see how the method | ||
211 | 1564 | looks in 1.0. It's still called 'new_name', and its 'required' | ||
212 | 1565 | argument is still called 'required_argument', but its 'fixed' argument | ||
213 | 1566 | is fixed to the string '1.0 value'. | ||
214 | 1567 | |||
215 | 1568 | >>> ignored = dictionary.pop() | ||
216 | 1569 | >>> print dictionary['as'] | ||
217 | 1570 | new_name | ||
218 | 1571 | >>> dictionary['call_with'] | ||
219 | 1572 | {'fixed': '1.0 value'} | ||
220 | 1573 | >>> print dictionary['params']['required'].__name__ | ||
221 | 1574 | required_argument | ||
222 | 1575 | >>> dictionary['cache_for'] | ||
223 | 1576 | 100 | ||
224 | 1577 | |||
225 | 1578 | Let's pop one more time to see how the method looks in the pre-1.0 | ||
226 | 1579 | version. It hasn't yet been renamed to 'new_name', its 'required' | ||
227 | 1580 | argument hasn't yet been renamed to 'required_argument', and its | ||
228 | 1581 | 'fixed' argument is fixed to the string 'pre-1.0 value'. | ||
229 | 1582 | |||
230 | 1583 | >>> ignored = dictionary.pop() | ||
231 | 1584 | >>> print dictionary.get('as') | ||
232 | 1585 | None | ||
233 | 1586 | >>> print dictionary['params']['required'].__name__ | ||
234 | 1587 | required | ||
235 | 1588 | >>> dictionary['call_with'] | ||
236 | 1589 | {'fixed': 'pre-1.0 value'} | ||
237 | 1590 | >>> dictionary['cache_for'] | ||
238 | 1591 | 100 | ||
239 | 1592 | |||
240 | 1593 | @operation_removed_in_version | ||
241 | 1594 | ***************************** | ||
242 | 1595 | |||
243 | 1596 | Sometimes you want version n+1 to remove a named operation that was | ||
244 | 1597 | present in version n. The @operation_removed_in_version declaration | ||
245 | 1598 | does just this. | ||
246 | 1599 | |||
247 | 1600 | Let's define an operation that's introduced in 1.0 and removed in 2.0. | ||
248 | 1601 | |||
249 | 1602 | >>> from lazr.restful.declarations import operation_removed_in_version | ||
250 | 1603 | >>> class DisappearingMultiversionMethod(Interface): | ||
251 | 1604 | ... export_as_webservice_entry() | ||
252 | 1605 | ... @operation_removed_in_version(2.0) | ||
253 | 1606 | ... @operation_parameters(arg=Float()) | ||
254 | 1607 | ... @export_read_operation() | ||
255 | 1608 | ... @operation_for_version(1.0) | ||
256 | 1609 | ... def method(arg): | ||
257 | 1610 | ... """A doomed method.""" | ||
258 | 1611 | |||
259 | 1612 | >>> dictionary = DisappearingMultiversionMethod[ | ||
260 | 1613 | ... 'method'].getTaggedValue('lazr.restful.exported') | ||
261 | 1614 | |||
262 | 1615 | The method is not present in 2.0: | ||
263 | 1616 | |||
264 | 1617 | >>> version, attrs = dictionary.pop() | ||
265 | 1618 | >>> print version | ||
266 | 1619 | 2.0 | ||
267 | 1620 | >>> sorted(attrs.items()) | ||
268 | 1621 | [] | ||
269 | 1622 | |||
270 | 1623 | It is present in 1.0: | ||
271 | 1624 | |||
272 | 1625 | >>> version, attrs = dictionary.pop() | ||
273 | 1626 | >>> print version | ||
274 | 1627 | 1.0 | ||
275 | 1628 | >>> print attrs['type'] | ||
276 | 1629 | read_operation | ||
277 | 1630 | >>> print attrs['params']['arg'] | ||
278 | 1631 | <zope.schema._field.Float object...> | ||
279 | 1632 | |||
280 | 1633 | But it's not present in the unnamed pre-1.0 version, since it hadn't | ||
281 | 1634 | been defined yet: | ||
282 | 1635 | |||
283 | 1636 | >>> print dictionary.pop() | ||
284 | 1637 | (None, {}) | ||
285 | 1638 | |||
286 | 1639 | The @operation_removed_in_version declaration can also be used to | ||
287 | 1640 | reset a named operation's definition if you need to completely re-do | ||
288 | 1641 | it. | ||
289 | 1642 | |||
290 | 1643 | For instance, ordinarily you can't change the type of an operation, or | ||
291 | 1644 | totally redefine its parameters--and you shouldn't really need | ||
292 | 1645 | to. It's usually easier to publish two different operations that have | ||
293 | 1646 | the same name in different versions. But you can do it with a single | ||
294 | 1647 | operation, by removing the operation with | ||
295 | 1648 | @operation_removed_in_version and defining it again--either in the | ||
296 | 1649 | same version or in some later version. | ||
297 | 1650 | |||
298 | 1651 | In this example, the type of the operation, the type and number of the | ||
299 | 1652 | arguments, and the return value change in version 1.0. | ||
300 | 1653 | |||
301 | 1654 | >>> class ReadOrWriteMethod(Interface): | ||
302 | 1655 | ... export_as_webservice_entry() | ||
303 | 1656 | ... | ||
304 | 1657 | ... @operation_parameters(arg=TextLine(), arg2=TextLine()) | ||
305 | 1658 | ... @export_write_operation() | ||
306 | 1659 | ... @operation_removed_in_version(1.0) | ||
307 | 1660 | ... | ||
308 | 1661 | ... @operation_parameters(arg=Float()) | ||
309 | 1662 | ... @operation_returns_collection_of(Interface) | ||
310 | 1663 | ... @export_read_operation() | ||
311 | 1664 | ... def method(arg, arg2='default'): | ||
312 | 1665 | ... """A read *or* a write operation, depending on version.""" | ||
313 | 1666 | |||
314 | 1667 | >>> dictionary = ReadOrWriteMethod[ | ||
315 | 1668 | ... 'method'].getTaggedValue('lazr.restful.exported') | ||
316 | 1669 | |||
317 | 1670 | In version 1.0, the 'method' named operation is a write operation that | ||
318 | 1671 | takes two TextLine arguments and has no special return value. | ||
319 | 1672 | |||
320 | 1673 | >>> version, attrs = dictionary.pop() | ||
321 | 1674 | >>> print version | ||
322 | 1675 | 1.0 | ||
323 | 1676 | >>> print attrs['type'] | ||
324 | 1677 | write_operation | ||
325 | 1678 | >>> attrs['params']['arg'] | ||
326 | 1679 | <zope.schema._bootstrapfields.TextLine object...> | ||
327 | 1680 | >>> attrs['params']['arg2'] | ||
328 | 1681 | <zope.schema._bootstrapfields.TextLine object...> | ||
329 | 1682 | >>> print attrs.get('return_type') | ||
330 | 1683 | None | ||
331 | 1684 | |||
332 | 1685 | In the unnamed pre-1.0 version, the 'method' operation is a read | ||
333 | 1686 | operation that takes a single Float argument and returns a collection. | ||
334 | 1687 | |||
335 | 1688 | >>> version, attrs = dictionary.pop() | ||
336 | 1689 | >>> print attrs['type'] | ||
337 | 1690 | read_operation | ||
338 | 1691 | |||
339 | 1692 | >>> attrs['params']['arg'] | ||
340 | 1693 | <zope.schema._field.Float object...> | ||
341 | 1694 | >>> attrs['params'].keys() | ||
342 | 1695 | ['arg'] | ||
343 | 1696 | |||
344 | 1697 | >>> attrs['return_type'] | ||
345 | 1698 | <lazr.restful.fields.CollectionField object...> | ||
346 | 1699 | |||
347 | 1700 | [XXX leonardr mutator_for modifies the field, not the method, so it | ||
348 | 1701 | won't work until I add multiversion support for fields. Also, it's | ||
349 | 1702 | possible to remove a named operation from a certain version and then | ||
350 | 1703 | uselessly annotate it some more. The best place to catch this error | ||
351 | 1704 | turns out to be when generating the adapter classes, not when | ||
352 | 1705 | collecting annotations.] | ||
353 | 1706 | |||
354 | 1580 | Security | 1707 | Security |
355 | 1581 | ======== | 1708 | ======== |
356 | 1582 | 1709 |
This branch introduces the @operation_ removed_ in_version method, which stops an operation from having any configuration for the given version number. This has two uses: 1. to remove a named operation as of a current version. 2. to reset the named operation's configuration so that you can do something strange to it like change a read operation to a write operation. My test gives examples of both.
Most of the changes to the doctest are caused by my moving the test elsewhere in the document.