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

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
Reviewer Review Type Date Requested Status
Gary Poster Approve
Review via email: mp+17784@code.launchpad.net
To post a comment you must log in.
Revision history for this message
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.

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 # 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