Merge lp:~leonardr/lazr.restful/operation-removed-in into lp:lazr.restful

Proposed by Leonard Richardson on 2010-01-20
Status: Merged
Approved by: Gary Poster on 2010-01-20
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
Reviewer Review Type Date Requested Status
Gary Poster 2010-01-20 Approve on 2010-01-20
Review via email: mp+17784@code.launchpad.net
To post a comment you must log in.
Leonard Richardson (leonardr) wrote :

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.

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 # in the interface method specification.
6 annotations = method.__dict__.get(LAZR_WEBSERVICE_EXPORTED, None)
7 if annotations is None:
8- # Create a new bleed-through dict which associates
9+ # Create a new versioned dict which associates
10 # annotation data with the earliest active version of the
11 # web service. Future @webservice_version annotations will
12 # push later versions onto the VersionedDict, allowing
13@@ -475,12 +475,27 @@
14 def annotate_method(self, method, annotations):
15 """See `_method_annotator`."""
16 # The annotations dict is a VersionedDict. Push a new dict
17- # onto its stack, labeled with the version number, so that
18- # future annotations can override old annotations without
19- # destroying them.
20+ # onto its stack, labeled with the version number, and copy in
21+ # the old version's annotations so that this version can
22+ # modify those annotations without destroying them.
23 annotations.push(self.version)
24
25
26+class operation_removed_in_version(operation_for_version):
27+ """Decoration removing this operation from the web service.
28+
29+ This operation will not be present in the given version of the web
30+ service, or any subsequent version, unless it's re-published with
31+ an export_*_operation method.
32+ """
33+ def annotate_method(self, method, annotations):
34+ """See `_method_annotator`."""
35+ # The annotations dict is a VersionedDict. Push a new dict
36+ # onto its stack, labeled with the version number. Make sure the
37+ # new dict is empty rather than copying the old annotations
38+ annotations.push(self.version, True)
39+
40+
41 class export_operation_as(_method_annotator):
42 """Decorator specifying the name to export the method as."""
43
44
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 >>> param_defs['optional2'].default
50 u'Default2'
51
52-Versioning
53-----------
54-
55-Different versions of the webservice can publish the same interface
56-method in totally different ways. Here's a simple example. This method
57-appears differently in three versions of the web service: 2.0, 1.0,
58-and in an unnamed pre-1.0 version.
59-
60- >>> from lazr.restful.declarations import operation_for_version
61- >>> class MultiVersionMethod(Interface):
62- ... export_as_webservice_entry()
63- ...
64- ... @call_with(fixed='2.0 value')
65- ... @operation_for_version('2.0')
66- ...
67- ... @call_with(fixed='1.0 value')
68- ... @export_operation_as('new_name')
69- ... @rename_parameters_as(required="required_argument")
70- ... @operation_for_version('1.0')
71- ...
72- ... @call_with(fixed='pre-1.0 value')
73- ... @operation_parameters(
74- ... required=TextLine(),
75- ... fixed=TextLine()
76- ... )
77- ... @export_read_operation()
78- ... def a_method(required, fixed='Fixed value'):
79- ... """Method demonstrating multiversion publication."""
80-
81-The tagged value containing the annotations looks like a dictionary,
82-but it's actually a stack of dictionaries named after the versions.
83-
84- >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue(
85- ... 'lazr.restful.exported')
86- >>> dictionary.dict_names
87- [None, '1.0', '2.0']
88-
89-The dictionary on top of the stack is for the 2.0 version of the web
90-service. In 2.0, the method is published as 'new_name' and its 'fixed'
91-argument is fixed to the string '2.0 value'.
92-
93- >>> print dictionary['as']
94- new_name
95- >>> dictionary['call_with']
96- {'fixed': '2.0 value'}
97-
98-The published name of the 'required' argument is 'required_argument',
99-not 'required'.
100-
101- >>> print dictionary['params']['required'].__name__
102- required_argument
103-
104-Let's pop the 2.0 version off the stack. Now we can see how the method
105-looks in 1.0. It's still called 'new_name', and its 'required'
106-argument is still called 'required_argument', but its 'fixed' argument
107-is fixed to the string '1.0 value'.
108-
109- >>> ignored = dictionary.pop()
110- >>> print dictionary['as']
111- new_name
112- >>> dictionary['call_with']
113- {'fixed': '1.0 value'}
114- >>> print dictionary['params']['required'].__name__
115- required_argument
116-
117-Let's pop one more time to see how the method looks in the pre-1.0
118-version. It hasn't yet been renamed to 'new_name', its 'required'
119-argument hasn't yet been renamed to 'required_argument', and its
120-'fixed' argument is fixed to the string 'pre-1.0 value'.
121-
122- >>> ignored = dictionary.pop()
123- >>> print dictionary.get('as')
124- None
125- >>> print dictionary['params']['required'].__name__
126- required
127- >>> dictionary['call_with']
128- {'fixed': 'pre-1.0 value'}
129-
130-
131 Error handling
132 --------------
133
134@@ -1526,8 +1447,8 @@
135 It is possible to cache a server response in the browser cache using
136 the @cache_for decorator:
137
138+ >>> from lazr.restful.testing.webservice import FakeRequest
139 >>> from lazr.restful.declarations import cache_for
140- >>> from lazr.restful.testing.webservice import FakeRequest
141 >>>
142 >>> class ICachedBookSet(IBookSet):
143 ... """IBookSet supporting caching."""
144@@ -1577,6 +1498,212 @@
145 ...
146 ValueError: Caching duration should be a positive number: -15
147
148+Versioned services
149+==================
150+
151+Different versions of the webservice can publish the same data model
152+object in totally different ways.
153+
154+Named operations
155+----------------
156+
157+It's easy to reflect the most common changes between versions:
158+operations and arguments being renamed, changes in fixed values, etc.
159+This method appears differently in three versions of the web service:
160+2.0, 1.0, and in an unnamed pre-1.0 version.
161+
162+ >>> from lazr.restful.declarations import operation_for_version
163+ >>> class MultiVersionMethod(Interface):
164+ ... export_as_webservice_entry()
165+ ...
166+ ... @call_with(fixed='2.0 value')
167+ ... @cache_for(300)
168+ ... @operation_for_version('2.0')
169+ ...
170+ ... @call_with(fixed='1.0 value')
171+ ... @export_operation_as('new_name')
172+ ... @rename_parameters_as(required="required_argument")
173+ ... @operation_for_version('1.0')
174+ ...
175+ ... @call_with(fixed='pre-1.0 value')
176+ ... @cache_for(100)
177+ ... @operation_parameters(
178+ ... required=TextLine(),
179+ ... fixed=TextLine()
180+ ... )
181+ ... @export_read_operation()
182+ ... def a_method(required, fixed='Fixed value'):
183+ ... """Method demonstrating multiversion publication."""
184+
185+The tagged value containing the annotations looks like a dictionary,
186+but it's actually a stack of dictionaries named after the versions.
187+
188+ >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue(
189+ ... 'lazr.restful.exported')
190+ >>> dictionary.dict_names
191+ [None, '1.0', '2.0']
192+
193+The dictionary on top of the stack is for the 2.0 version of the web
194+service. In 2.0, the method is published as 'new_name' and its 'fixed'
195+argument is fixed to the string '2.0 value'.
196+
197+ >>> print dictionary['as']
198+ new_name
199+ >>> dictionary['call_with']
200+ {'fixed': '2.0 value'}
201+ >>> dictionary['cache_for']
202+ 300
203+
204+The published name of the 'required' argument is 'required_argument',
205+not 'required'.
206+
207+ >>> print dictionary['params']['required'].__name__
208+ required_argument
209+
210+Let's pop the 2.0 version off the stack. Now we can see how the method
211+looks in 1.0. It's still called 'new_name', and its 'required'
212+argument is still called 'required_argument', but its 'fixed' argument
213+is fixed to the string '1.0 value'.
214+
215+ >>> ignored = dictionary.pop()
216+ >>> print dictionary['as']
217+ new_name
218+ >>> dictionary['call_with']
219+ {'fixed': '1.0 value'}
220+ >>> print dictionary['params']['required'].__name__
221+ required_argument
222+ >>> dictionary['cache_for']
223+ 100
224+
225+Let's pop one more time to see how the method looks in the pre-1.0
226+version. It hasn't yet been renamed to 'new_name', its 'required'
227+argument hasn't yet been renamed to 'required_argument', and its
228+'fixed' argument is fixed to the string 'pre-1.0 value'.
229+
230+ >>> ignored = dictionary.pop()
231+ >>> print dictionary.get('as')
232+ None
233+ >>> print dictionary['params']['required'].__name__
234+ required
235+ >>> dictionary['call_with']
236+ {'fixed': 'pre-1.0 value'}
237+ >>> dictionary['cache_for']
238+ 100
239+
240+@operation_removed_in_version
241+*****************************
242+
243+Sometimes you want version n+1 to remove a named operation that was
244+present in version n. The @operation_removed_in_version declaration
245+does just this.
246+
247+Let's define an operation that's introduced in 1.0 and removed in 2.0.
248+
249+ >>> from lazr.restful.declarations import operation_removed_in_version
250+ >>> class DisappearingMultiversionMethod(Interface):
251+ ... export_as_webservice_entry()
252+ ... @operation_removed_in_version(2.0)
253+ ... @operation_parameters(arg=Float())
254+ ... @export_read_operation()
255+ ... @operation_for_version(1.0)
256+ ... def method(arg):
257+ ... """A doomed method."""
258+
259+ >>> dictionary = DisappearingMultiversionMethod[
260+ ... 'method'].getTaggedValue('lazr.restful.exported')
261+
262+The method is not present in 2.0:
263+
264+ >>> version, attrs = dictionary.pop()
265+ >>> print version
266+ 2.0
267+ >>> sorted(attrs.items())
268+ []
269+
270+It is present in 1.0:
271+
272+ >>> version, attrs = dictionary.pop()
273+ >>> print version
274+ 1.0
275+ >>> print attrs['type']
276+ read_operation
277+ >>> print attrs['params']['arg']
278+ <zope.schema._field.Float object...>
279+
280+But it's not present in the unnamed pre-1.0 version, since it hadn't
281+been defined yet:
282+
283+ >>> print dictionary.pop()
284+ (None, {})
285+
286+The @operation_removed_in_version declaration can also be used to
287+reset a named operation's definition if you need to completely re-do
288+it.
289+
290+For instance, ordinarily you can't change the type of an operation, or
291+totally redefine its parameters--and you shouldn't really need
292+to. It's usually easier to publish two different operations that have
293+the same name in different versions. But you can do it with a single
294+operation, by removing the operation with
295+@operation_removed_in_version and defining it again--either in the
296+same version or in some later version.
297+
298+In this example, the type of the operation, the type and number of the
299+arguments, and the return value change in version 1.0.
300+
301+ >>> class ReadOrWriteMethod(Interface):
302+ ... export_as_webservice_entry()
303+ ...
304+ ... @operation_parameters(arg=TextLine(), arg2=TextLine())
305+ ... @export_write_operation()
306+ ... @operation_removed_in_version(1.0)
307+ ...
308+ ... @operation_parameters(arg=Float())
309+ ... @operation_returns_collection_of(Interface)
310+ ... @export_read_operation()
311+ ... def method(arg, arg2='default'):
312+ ... """A read *or* a write operation, depending on version."""
313+
314+ >>> dictionary = ReadOrWriteMethod[
315+ ... 'method'].getTaggedValue('lazr.restful.exported')
316+
317+In version 1.0, the 'method' named operation is a write operation that
318+takes two TextLine arguments and has no special return value.
319+
320+ >>> version, attrs = dictionary.pop()
321+ >>> print version
322+ 1.0
323+ >>> print attrs['type']
324+ write_operation
325+ >>> attrs['params']['arg']
326+ <zope.schema._bootstrapfields.TextLine object...>
327+ >>> attrs['params']['arg2']
328+ <zope.schema._bootstrapfields.TextLine object...>
329+ >>> print attrs.get('return_type')
330+ None
331+
332+In the unnamed pre-1.0 version, the 'method' operation is a read
333+operation that takes a single Float argument and returns a collection.
334+
335+ >>> version, attrs = dictionary.pop()
336+ >>> print attrs['type']
337+ read_operation
338+
339+ >>> attrs['params']['arg']
340+ <zope.schema._field.Float object...>
341+ >>> attrs['params'].keys()
342+ ['arg']
343+
344+ >>> attrs['return_type']
345+ <lazr.restful.fields.CollectionField object...>
346+
347+[XXX leonardr mutator_for modifies the field, not the method, so it
348+won't work until I add multiversion support for fields. Also, it's
349+possible to remove a named operation from a certain version and then
350+uselessly annotate it some more. The best place to catch this error
351+turns out to be when generating the adapter classes, not when
352+collecting annotations.]
353+
354 Security
355 ========
356

Subscribers

People subscribed via source and target branches