Merge lp:~leonardr/lazr.restful/use-bleedthrough-for-methods into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/use-bleedthrough-for-methods
Merge into: lp:lazr.restful
Diff against target: 244 lines (+132/-10)
4 files modified
src/lazr/restful/declarations.py (+36/-2)
src/lazr/restful/docs/utils.txt (+3/-0)
src/lazr/restful/docs/webservice-declarations.txt (+82/-4)
src/lazr/restful/utils.py (+11/-4)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/use-bleedthrough-for-methods
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+17683@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch creates the 'operation_for_version' annotation, used to publish a named operation in different ways for different versions of the web service. Currently 'operation_for_version' lets you define different versions of the methods, but it doesn't really matter, because the bleed-through stack means that only the most recent version is visible.

I made some changes to BleedThroughDict, implementing additional bits of the dict interface, so that the code in declarations.py could treat a BTD like a normal dict.

Not every annotation works with operation_for_version. For instance, the rename_parameters_as annotation directly modifies an object in the bleed-through stack, instead of putting new values in there. This means that using rename_parameters_as will modify every version of the annotation at once. In subsequent branches I'll be changing the BleedThroughDict data structure to work with this kind of annotation.

Revision history for this message
Brad Crittenden (bac) wrote :

Very nice branch Leonard. On IRC I suggested adding a comment about the use of None on the stack and you proposed a nice solution. With that change it looks great.

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-01-11 18:27:43 +0000
3+++ src/lazr/restful/declarations.py 2010-01-19 20:04:14 +0000
4@@ -27,6 +27,7 @@
5 'generate_entry_interface',
6 'generate_operation_adapter',
7 'mutator_for',
8+ 'operation_for_version',
9 'operation_parameters',
10 'operation_returns_entry',
11 'operation_returns_collection_of',
12@@ -56,7 +57,9 @@
13 from lazr.restful import (
14 Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink)
15 from lazr.restful.security import protect_schema
16-from lazr.restful.utils import camelcase_to_underscore_separated, get_current_browser_request
17+from lazr.restful.utils import (
18+ BleedThroughDict, camelcase_to_underscore_separated,
19+ get_current_browser_request)
20
21 LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS
22 COLLECTION_TYPE = 'collection'
23@@ -294,7 +297,17 @@
24 """Annotates the function with the fixed arguments."""
25 # Everything in the function dictionary ends up as tagged value
26 # in the interface method specification.
27- annotations = method.__dict__.setdefault(LAZR_WEBSERVICE_EXPORTED, {})
28+ annotations = method.__dict__.get(LAZR_WEBSERVICE_EXPORTED, None)
29+ if annotations is None:
30+ # Create a new bleed-through dict which associates
31+ # annotation data with the earliest active version of the
32+ # web service. Future @webservice_version annotations will
33+ # push later versions onto the BleedThroughDict, allowing
34+ # new versions to specify annotation data that conflicts
35+ # with old versions.
36+ annotations = BleedThroughDict()
37+ annotations.push(None)
38+ method.__dict__[LAZR_WEBSERVICE_EXPORTED] = annotations
39 self.annotate_method(method, annotations)
40 return method
41
42@@ -442,6 +455,27 @@
43 set(annotations.get('call_with', {}).keys()))
44
45
46+class operation_for_version(_method_annotator):
47+ """Decorator specifying which version of the webservice is defined.
48+
49+ Decorators processed after this one will decorate the given web
50+ service version and, by default, subsequent versions will inherit
51+ their values. Subsequent versions may provide conflicting values,
52+ but those values will not affect this version.
53+ """
54+ def __init__(self, version):
55+ _check_called_from_interface_def('%s()' % self.__class__.__name__)
56+ self.version = version
57+
58+ def annotate_method(self, method, annotations):
59+ """See `_method_annotator`."""
60+ # The annotations dict is a BleedThroughDict. Push a new dict
61+ # onto its stack, labeled with the version number, so that
62+ # future annotations can override old annotations without
63+ # destroying them.
64+ annotations.push(self.version)
65+
66+
67 class export_operation_as(_method_annotator):
68 """Decorator specifying the name to export the method as."""
69
70
71=== modified file 'src/lazr/restful/docs/utils.txt'
72--- src/lazr/restful/docs/utils.txt 2010-01-14 10:00:38 +0000
73+++ src/lazr/restful/docs/utils.txt 2010-01-19 20:04:14 +0000
74@@ -57,6 +57,9 @@
75 >>> print stack.get('key', 'default')
76 value
77
78+ >>> 'key' in stack
79+ True
80+
81 >>> sorted(stack.items())
82 [('key', 'value')]
83
84
85=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
86--- src/lazr/restful/docs/webservice-declarations.txt 2010-01-11 18:27:43 +0000
87+++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-19 20:04:14 +0000
88@@ -267,11 +267,12 @@
89 Exporting methods
90 =================
91
92-Entries and collections can support operations on the webservice. The
93-operations supported are defined by tagging methods in the content
94-interface with special decorators.
95+Entries and collections can publish named operations on the
96+webservice. Every named operation corresponds to some method defined
97+on the content interface. To publish a method as a named operation,
98+you tag it with special decorators.
99
100-Three different decorators are used based on the kind of method
101+Four different decorators are used based on the kind of method
102 exported.
103
104 1. @export_read_operation
105@@ -295,6 +296,11 @@
106 creating and the name of the fields in the schema that are passed as
107 parameters.
108
109+4. @export_destructor_operation
110+
111+ This will mark the method as available as a DELETE operation on the
112+ exported resource.
113+
114 The specification of the web service's acceptable method parameters
115 should be described using the @operation_parameters decorator, which
116 takes normal IField instances.
117@@ -444,6 +450,9 @@
118 return_type: <lazr.restful._operation.ObjectLink object...>
119 type: 'factory'
120
121+Default values and required parameters
122+--------------------------------------
123+
124 Parameters default and required attributes are set automatically based
125 on the method signature.
126
127@@ -494,6 +503,75 @@
128 >>> param_defs['optional2'].default
129 u'Default2'
130
131+Versioning
132+----------
133+
134+Different versions of the webservice can publish the same interface
135+method in totally different ways. Here's a simple example. This method
136+appears differently in three versions of the web service: 2.0, 1.0,
137+and in an unnamed pre-1.0 version.
138+
139+ >>> from lazr.restful.declarations import operation_for_version
140+ >>> class MultiVersionMethod(Interface):
141+ ... export_as_webservice_entry()
142+ ...
143+ ... @call_with(fixed='2.0 value')
144+ ... @operation_for_version('2.0')
145+ ...
146+ ... @call_with(fixed='1.0 value')
147+ ... @export_operation_as('new_name')
148+ ... @operation_for_version('1.0')
149+ ...
150+ ... @call_with(fixed='pre-1.0 value')
151+ ... @operation_parameters(
152+ ... required=TextLine(),
153+ ... fixed=TextLine()
154+ ... )
155+ ... @export_read_operation()
156+ ... def a_method(required, fixed='Fixed value'):
157+ ... """Method demonstrating multiversion publication."""
158+
159+The tagged value containing the annotations looks like a dictionary,
160+but it's actually a stack of dictionaries named after the versions.
161+
162+ >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue(
163+ ... 'lazr.restful.exported')
164+ >>> dictionary.dict_names
165+ [None, '1.0', '2.0']
166+
167+The dictionary on top of the stack is for the 2.0 version of the web
168+service. In 2.0, the method is published as 'new_name' and its 'fixed'
169+argument is fixed to the string '2.0 value'.
170+
171+ >>> print dictionary['as']
172+ new_name
173+ >>> dictionary['call_with']
174+ {'fixed': '2.0 value'}
175+
176+Let's pop the 2.0 version off the stack. Now we can see how the method
177+looks in 1.0. It's still called 'new_name', but its 'fixed' argument
178+is fixed to the string '1.0 value'.
179+
180+ >>> ignored = dictionary.pop()
181+ >>> print dictionary['as']
182+ new_name
183+ >>> dictionary['call_with']
184+ {'fixed': '1.0 value'}
185+
186+Let's pop one more time to see how the method looks in the pre-1.0
187+version. It hasn't yet been renamed to 'new_name', and its 'fixed'
188+argument is fixed to the string 'pre-1.0 value'.
189+
190+ >>> ignored = dictionary.pop()
191+ >>> print dictionary.get('as')
192+ None
193+ >>> dictionary['call_with']
194+ {'fixed': 'pre-1.0 value'}
195+ >>> print dictionary.items()
196+
197+Error handling
198+--------------
199+
200 All these decorators can only be used from within an interface
201 definition:
202
203
204=== modified file 'src/lazr/restful/utils.py'
205--- src/lazr/restful/utils.py 2010-01-14 17:08:20 +0000
206+++ src/lazr/restful/utils.py 2010-01-19 20:04:14 +0000
207@@ -41,9 +41,11 @@
208 """
209 missing = object()
210
211- def __init__(self):
212+ def __init__(self, name=None, dictionary=None, opaque=False):
213 """Initialize the bleed-through dictionary."""
214 self.stack = []
215+ if name is not None and dictionary is not None:
216+ self.push(name, dictionary, opaque)
217
218 def push(self, name, dictionary=None, opaque=False):
219 """Pushes a dictionary onto the stack.
220@@ -53,9 +55,6 @@
221 :arg opaque: If True, values will not 'bleed through' from
222 dictionaries lower on the stack.
223 """
224- if name is None:
225- self.stack.append(name)
226- return
227 if dictionary is None:
228 dictionary = {}
229 self.stack.append((name, dict(dictionary), opaque))
230@@ -83,6 +82,14 @@
231 """Is the stack empty?"""
232 return len(self.stack) == 0
233
234+ def setdefault(self, key, value):
235+ """Get a from the top of the stack, setting it if not present."""
236+ return self.stack[0][1].setdefault(key, value)
237+
238+ def __contains__(self, key):
239+ """Check whether a key is visible in the stack."""
240+ return self.get(key, missing) is not missing
241+
242 def __getitem__(self, key):
243 """Look up an item somewhere in the stack."""
244 if self.is_empty:

Subscribers

People subscribed via source and target branches