Merge lp:~wallyworld/lazr.restful/dict-parameters into lp:lazr.restful

Proposed by Ian Booth
Status: Merged
Merged at revision: 199
Proposed branch: lp:~wallyworld/lazr.restful/dict-parameters
Merge into: lp:lazr.restful
Diff against target: 351 lines (+180/-12)
6 files modified
.bzrignore (+1/-0)
src/lazr/restful/NEWS.txt (+6/-0)
src/lazr/restful/configure.zcml (+7/-0)
src/lazr/restful/docs/webservice-marshallers.txt (+83/-6)
src/lazr/restful/marshallers.py (+82/-5)
src/lazr/restful/version.txt (+1/-1)
To merge this branch: bzr merge lp:~wallyworld/lazr.restful/dict-parameters
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Approve
Benji York Pending
Review via email: mp+97128@code.launchpad.net

Commit message

Add support for marshalling exported method parameters of type dict.

Description of the change

== Implementation ==

Add the zcml registration and implementation class for a dictionary marshaller: DictFieldMarshaller.

Also fixed some drive-by lint issues.

== Tests ==

Add new tests to the webservice-marshallers.txt doc test.

== Lint ==

Linting changed files:
  src/lazr/restful/NEWS.txt
  src/lazr/restful/configure.zcml
  src/lazr/restful/marshallers.py
  src/lazr/restful/version.txt
  src/lazr/restful/docs/webservice-marshallers.txt

./src/lazr/restful/marshallers.py
     157: E261 at least two spaces before inline comment

To post a comment you must log in.
201. By Ian Booth

Fix unmarshalling implementation

202. By Ian Booth

Remove debugging code

203. By Ian Booth

Add support for marshalling single element tuples as a string

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

Hi Ian,

Thank you very much for this addition to lazr.restful!

I'm approving this on the premise that you'll address my main comment.

[1]

255 + def __init__(self, field, request):
256 + """See `SimpleFieldMarshaller`.
257 +
258 + This also looks for the appropriate marshaller for key_type and
259 + value_type.
260 + """
261 + super(DictFieldMarshaller, self).__init__(
262 + field, request)
263 + self.key_marshaller = getMultiAdapter(
264 + (field.key_type, request), IFieldMarshaller)
265 + self.value_marshaller = getMultiAdapter(
266j+ (field.value_type, request), IFieldMarshaller)

I think we should probably raise an error if key_type or value_type aren't defined?

[2]

307 + @property
308 + def _python_collection_factory(self):
309 + """Create the appropriate python collection from a dict."""
310 + # For the DictMarshaller, _type contains the type object,
311 + # which can be used as a factory.
312 + return self.field._type

Why this indirection? Do you really expect users to subclass the marshaller to
override the default? YAGNI

[3]

300 + value = super(
301 + DictFieldMarshaller, self)._marshall_from_json_data(value)

Why is this needed?

value is already a dict, what do you expect the superclass to handle? Ah,
I think it's error handling? Maybe add a comment about that? Which actually
points to the fact that you are missing a test for this. You test that ValueError
are raised if the key or value are of the improper type, but not if the request/json is
not actually a dict (or of the proper representation).

Thanks for the drive-by formatting fixes.

Cheers

review: Approve
Revision history for this message
Ian Booth (wallyworld) wrote :

Hi

Thanks for the review

>
> [1]
>
> 255 + def __init__(self, field, request):
> 256 + """See `SimpleFieldMarshaller`.
> 257 +
> 258 + This also looks for the appropriate marshaller for key_type and
> 259 + value_type.
> 260 + """
> 261 + super(DictFieldMarshaller, self).__init__(
> 262 + field, request)
> 263 + self.key_marshaller = getMultiAdapter(
> 264 + (field.key_type, request), IFieldMarshaller)
> 265 + self.value_marshaller = getMultiAdapter(
> 266j+ (field.value_type, request), IFieldMarshaller)
>
> I think we should probably raise an error if key_type or value_type aren't defined?
>

There's no need - the default adaptor lookup will simply return a
SimpleFieldMarshaller. The AbstractCollectionFieldMarshaller which is
used for List and Set also works the same way.

>
> [2]
>
> 307 + @property
> 308 + def _python_collection_factory(self):
> 309 + """Create the appropriate python collection from a dict."""
> 310 + # For the DictMarshaller, _type contains the type object,
> 311 + # which can be used as a factory.
> 312 + return self.field._type
>
> Why this indirection? Do you really expect users to subclass the marshaller to
> override the default? YAGNI
>

I based the code on what AbstractCollectionFieldMarshaller does, but in
this case we know it's a dict so I agree YAGNI, will fix.

> [3]
>
> 300 + value = super(
> 301 + DictFieldMarshaller, self)._marshall_from_json_data(value)
>
> Why is this needed?
>
> value is already a dict, what do you expect the superclass to handle? Ah,
> I think it's error handling? Maybe add a comment about that? Which actually
> points to the fact that you are missing a test for this. You test that ValueError
> are raised if the key or value are of the improper type, but not if the request/json is
> not actually a dict (or of the proper representation).
>

Yes, it's for error handling. I'll add a comment and yes, I did miss out
adding a test for this (I meant to). Thanks for picking that up.

Revision history for this message
Ian Booth (wallyworld) wrote :

Argh, poor wording.

>
> Yes, it's for error handling. I'll add a comment and yes, I did miss out
> adding a test for this (I meant to). Thanks for picking that up.
>

I mean to say that I had intended to add a test but somehow didn't.

204. By Ian Booth

Add better error reporting and update test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-11-18 22:23:44 +0000
3+++ .bzrignore 2012-03-13 23:50:24 +0000
4@@ -12,3 +12,4 @@
5 *.egg
6 dist
7 .DS_Store
8+MANIFEST
9
10=== modified file 'src/lazr/restful/NEWS.txt'
11--- src/lazr/restful/NEWS.txt 2011-10-11 01:45:29 +0000
12+++ src/lazr/restful/NEWS.txt 2012-03-13 23:50:24 +0000
13@@ -2,6 +2,12 @@
14 NEWS for lazr.restful
15 =====================
16
17+0.19.5 (2012-03-13)
18+===================
19+
20+Fixed bug 953587: add a dict marshaller so that exported method parameters
21+can be of type dict.
22+
23 0.19.4 (2011-10-11)
24 ===================
25
26
27=== modified file 'src/lazr/restful/configure.zcml'
28--- src/lazr/restful/configure.zcml 2011-03-22 14:42:59 +0000
29+++ src/lazr/restful/configure.zcml 2012-03-13 23:50:24 +0000
30@@ -272,6 +272,13 @@
31 />
32
33 <adapter
34+ for="zope.schema.interfaces.IDict
35+ zope.publisher.interfaces.http.IHTTPRequest"
36+ provides="lazr.restful.interfaces.IFieldMarshaller"
37+ factory="lazr.restful.marshallers.DictFieldMarshaller"
38+ />
39+
40+ <adapter
41 for="lazr.restful.interfaces.ICollectionField
42 zope.publisher.interfaces.http.IHTTPRequest"
43 provides="lazr.restful.interfaces.IFieldMarshaller"
44
45=== modified file 'src/lazr/restful/docs/webservice-marshallers.txt'
46--- src/lazr/restful/docs/webservice-marshallers.txt 2010-10-05 15:49:55 +0000
47+++ src/lazr/restful/docs/webservice-marshallers.txt 2012-03-13 23:50:24 +0000
48@@ -700,9 +700,10 @@
49 collection of objects associated with some other object. The generic
50 collection marshaller will take care of marshalling to the proper
51 collection type, and of marshalling the individual items using the
52-marshaller for its value_type.
53+marshaller for its value_type. Dictionaries may specify separate
54+marshallers for their keys and values.
55
56- >>> from zope.schema import List, Tuple, Set
57+ >>> from zope.schema import Dict, List, Tuple, Set
58 >>> list_of_strings_field = List(value_type=Text())
59 >>> from lazr.restful.example.base.interfaces import Cuisine
60 >>> tuple_of_ints_field = Tuple(value_type=Int())
61@@ -710,6 +711,9 @@
62 ... value_type=Choice(vocabulary=Cuisine))
63 >>> set_of_choices_field = Set(
64 ... value_type=Choice(vocabulary=Cuisine)).bind(None)
65+ >>> dict_of_choices_field = Dict(
66+ ... key_type=Text(),
67+ ... value_type=Choice(vocabulary=Cuisine))
68
69 >>> list_marshaller = getMultiAdapter(
70 ... (list_of_strings_field, request), IFieldMarshaller)
71@@ -731,9 +735,22 @@
72 >>> verifyObject(IFieldMarshaller, set_marshaller)
73 True
74
75-The only JSON representation for the collection itself is a list, since
76-that's the only sequence type available in JSON. Anything else will
77-raise a ValueError.
78+ >>> dict_marshaller = getMultiAdapter(
79+ ... (dict_of_choices_field, request), IFieldMarshaller)
80+ >>> verifyObject(IFieldMarshaller, dict_marshaller)
81+ True
82+ >>> dict_key_marshaller = getMultiAdapter(
83+ ... (dict_of_choices_field.key_type, request), IFieldMarshaller)
84+ >>> verifyObject(IFieldMarshaller, dict_key_marshaller)
85+ True
86+ >>> dict_value_marshaller = getMultiAdapter(
87+ ... (dict_of_choices_field.value_type, request), IFieldMarshaller)
88+ >>> verifyObject(IFieldMarshaller, dict_value_marshaller)
89+ True
90+
91+For sequences, the only JSON representation for the collection itself is a
92+list, since that's the only sequence type available in JSON. Anything else
93+will raise a ValueError.
94
95 >>> list_marshaller.marshall_from_json_data([u"Test"])
96 [u'Test']
97@@ -743,11 +760,40 @@
98 ...
99 ValueError: got 'unicode', expected list: u'Test'
100
101+For dicts, we support marshalling from sequences of (name, value) pairs as
102+well as from dicts or even strings which are interpreted as single element
103+lists.
104+
105+ >>> dict_marshaller.marshall_from_json_data({u"foo": u"Vegetarian"})
106+ {u'foo': <Item Cuisine.VEGETARIAN, ...>}
107+
108+ >>> dict_marshaller.marshall_from_json_data([(u"foo", u"Vegetarian")])
109+ {u'foo': <Item Cuisine.VEGETARIAN, ...>}
110+
111+ >>> dict_marshaller.marshall_from_request(u"foo,Vegetarian")
112+ {u'foo': <Item Cuisine.VEGETARIAN, ...>}
113+
114+If we attempt to marshall something other than one of the above data formats,
115+a ValueError will be raised.
116+
117+ >>> dict_marshaller.marshall_from_json_data(u"Test")
118+ Traceback (most recent call last):
119+ ...
120+ ValueError: got 'unicode', expected dict: u'Test'
121+
122+ >>> dict_marshaller.marshall_from_request(u"Test")
123+ Traceback (most recent call last):
124+ ...
125+ ValueError: got '[u'Test']', list of name,value pairs
126+
127 None is passed through though.
128
129 >>> print list_marshaller.marshall_from_json_data(None)
130 None
131
132+ >>> print dict_marshaller.marshall_from_json_data(None)
133+ None
134+
135 ValueError is also raised if one of the value in the list doesn't
136 validate against the more specific marshaller.
137
138@@ -760,6 +806,18 @@
139 ... [u'Vegetarian', u'NoSuchChoice'])
140 ValueError: Invalid value "NoSuchChoice"...
141
142+ValueError is also raised if one of the keys or values in the dict doesn't
143+validate against the more specific marshaller.
144+
145+ >>> dict_marshaller.marshall_from_json_data({1: u"Vegetarian"})
146+ Traceback (most recent call last):
147+ ...
148+ ValueError: got 'int', expected unicode: 1
149+
150+ >>> show_ValueError(dict_marshaller.marshall_from_request,
151+ ... {u'foo': u'NoSuchChoice'})
152+ ValueError: Invalid value "NoSuchChoice"...
153+
154 The return type is correctly typed to the concrete collection.
155
156 >>> tuple_marshaller.marshall_from_json_data([1, 2, 3])
157@@ -776,16 +834,28 @@
158 >>> [item.title for item in result]
159 ['Vegetarian', 'General']
160
161+ >>> marshalled_dict = dict_marshaller.marshall_from_json_data(
162+ ... {u'foo': u'Vegetarian', u'bar': u'General'})
163+ >>> type(marshalled_dict)
164+ <type 'dict'>
165+ >>> sorted(
166+ ... [(key, value.title) for key, value in marshalled_dict.items()])
167+ [(u'bar', 'General'), (u'foo', 'Vegetarian')]
168
169 When coming from the request, either a list or a JSON-encoded
170 representation is accepted. The normal request rules for the
171-underlying type are then followed.
172+underlying type are then followed. When marshalling dicts, the
173+list elements are name,value strings which are pulled apart and
174+used to populate the dict.
175
176 >>> list_marshaller.marshall_from_request([u'1', u'2'])
177 [u'1', u'2']
178 >>> list_marshaller.marshall_from_request('["1", "2"]')
179 [u'1', u'2']
180
181+ >>> dict_marshaller.marshall_from_request('["foo,Vegetarian"]')
182+ {u'foo': <Item Cuisine.VEGETARIAN, ...>}
183+
184 >>> tuple_marshaller.marshall_from_request([u'1', u'2'])
185 (1, 2)
186
187@@ -794,6 +864,9 @@
188 >>> print list_marshaller.marshall_from_request('null')
189 None
190
191+ >>> print dict_marshaller.marshall_from_request('null')
192+ None
193+
194 Also, as a convenience for web client, so that they don't have to JSON
195 encode single-element list, non-list value are promoted into a
196 single-element list.
197@@ -810,6 +883,10 @@
198 >>> sorted(set_marshaller.unmarshall(None, marshalled_set))
199 ['Dessert', 'Vegetarian']
200
201+ >>> unmarshalled = dict_marshaller.unmarshall(None, marshalled_dict)
202+ >>> sorted(unmarshalled.iteritems())
203+ [(u'bar', 'General'), (u'foo', 'Vegetarian')]
204+
205 CollectionField
206 ---------------
207
208
209=== modified file 'src/lazr/restful/marshallers.py'
210--- src/lazr/restful/marshallers.py 2011-01-21 21:12:17 +0000
211+++ src/lazr/restful/marshallers.py 2012-03-13 23:50:24 +0000
212@@ -9,6 +9,7 @@
213 'BytesFieldMarshaller',
214 'CollectionFieldMarshaller',
215 'DateTimeFieldMarshaller',
216+ 'DictFieldMarshaller',
217 'FloatFieldMarshaller',
218 'IntFieldMarshaller',
219 'ObjectLookupFieldMarshaller',
220@@ -112,7 +113,7 @@
221 path_parts = [urllib.unquote(part) for part in path.split('/')]
222 path_parts.pop(0)
223 path_parts.reverse()
224- request = config.createRequest(StringIO(), {'PATH_INFO' : path})
225+ request = config.createRequest(StringIO(), {'PATH_INFO': path})
226 request.setTraversalStack(path_parts)
227 root = request.publication.getApplication(self.request)
228 return request.traverse(root)
229@@ -125,7 +126,6 @@
230 """
231 implements(IFieldMarshaller)
232
233-
234 # Set this to type or tuple of types that the JSON value must be of.
235 _type = None
236
237@@ -153,7 +153,7 @@
238 if value != '':
239 try:
240 v = value
241- if isinstance (v, str):
242+ if isinstance(v, str):
243 v = v.decode('utf8') # assume utf8
244 # XXX gary 2009-03-28
245 # The use of the enclosing brackets is a hack to work around
246@@ -327,7 +327,7 @@
247 """Describe all values, not just the selected value."""
248 unmarshalled = []
249 for item in self.field.vocabulary:
250- item_dict = {'token' : item.token, 'title' : item.title}
251+ item_dict = {'token': item.token, 'title': item.title}
252 if value.title == item.title:
253 item_dict['selected'] = True
254 unmarshalled.append(item_dict)
255@@ -462,6 +462,84 @@
256 return set
257
258
259+class DictFieldMarshaller(SimpleFieldMarshaller):
260+ """A marshaller for Dicts.
261+
262+ It looks up the marshallers for its (key-type, value-type), to handle its
263+ contained name/value pairs.
264+ """
265+ _type = dict
266+ _type_error_message = 'not a dict: %r'
267+
268+ def __init__(self, field, request):
269+ """See `SimpleFieldMarshaller`.
270+
271+ This also looks for the appropriate marshaller for key_type and
272+ value_type.
273+ """
274+ super(DictFieldMarshaller, self).__init__(
275+ field, request)
276+ self.key_marshaller = getMultiAdapter(
277+ (field.key_type, request), IFieldMarshaller)
278+ self.value_marshaller = getMultiAdapter(
279+ (field.value_type, request), IFieldMarshaller)
280+
281+ def _marshall_from_request(self, value):
282+ """See `IFieldMarshaller`.
283+
284+ All items in the dict are marshalled using the appropriate
285+ `IFieldMarshaller` for the key_type, value_type.
286+
287+ When coming in via HTML request parameters, the dict is sent as a list
288+ of comma separated "key,value" strings.
289+ If the value isn't a list or dict, transform it into a one-element list.
290+ That allows web client to submit one-element list of strings
291+ without having to JSON-encode it.
292+ """
293+ if not isinstance(value, list) and not isinstance(value, dict):
294+ value = [value]
295+ if isinstance(value, list):
296+ try:
297+ value = dict(nv.split(',', 2) for nv in value)
298+ except ValueError:
299+ raise ValueError(
300+ u"got '%s', list of name,value pairs" % value)
301+ return dict(
302+ (self.key_marshaller.marshall_from_json_data(key),
303+ self.value_marshaller.marshall_from_json_data(val))
304+ for key, val in value.items())
305+
306+ def _marshall_from_json_data(self, value):
307+ """See `SimpleFieldMarshaller`.
308+
309+ Marshall the items in the dict using the appropriate
310+ (key,value) marshallers.
311+
312+ The incoming value may be a list of name,value pairs in which case
313+ we convert to a dict first.
314+ """
315+ if isinstance(value, list):
316+ value = dict(value)
317+ # Call super to check for errors and raise a ValueError if necessary.
318+ value = super(
319+ DictFieldMarshaller, self)._marshall_from_json_data(value)
320+ return dict(
321+ (self.key_marshaller.marshall_from_json_data(key),
322+ self.value_marshaller.marshall_from_json_data(val))
323+ for key, val in value.items())
324+
325+ def unmarshall(self, entry, value):
326+ """See `SimpleFieldMarshaller`.
327+
328+ The value is unmarshalled into a dict and all its items are
329+ unmarshalled using the appropriate FieldMarshaller.
330+ """
331+ return dict(
332+ (self.key_marshaller.unmarshall(entry, key),
333+ self.value_marshaller.unmarshall(entry, val))
334+ for key, val in value.items())
335+
336+
337 class CollectionFieldMarshaller(SimpleFieldMarshaller):
338 """A marshaller for collection fields."""
339 implements(IUnmarshallingDoesntNeedValue)
340@@ -569,4 +647,3 @@
341 # to the object underlying a resource, we need to strip its
342 # security proxy.
343 return removeSecurityProxy(resource).context
344-
345
346=== modified file 'src/lazr/restful/version.txt'
347--- src/lazr/restful/version.txt 2011-10-11 01:45:29 +0000
348+++ src/lazr/restful/version.txt 2012-03-13 23:50:24 +0000
349@@ -1,1 +1,1 @@
350-0.19.4
351+0.19.5

Subscribers

People subscribed via source and target branches