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
=== modified file '.bzrignore'
--- .bzrignore 2011-11-18 22:23:44 +0000
+++ .bzrignore 2012-03-13 23:50:24 +0000
@@ -12,3 +12,4 @@
12*.egg12*.egg
13dist13dist
14.DS_Store14.DS_Store
15MANIFEST
1516
=== modified file 'src/lazr/restful/NEWS.txt'
--- src/lazr/restful/NEWS.txt 2011-10-11 01:45:29 +0000
+++ src/lazr/restful/NEWS.txt 2012-03-13 23:50:24 +0000
@@ -2,6 +2,12 @@
2NEWS for lazr.restful2NEWS for lazr.restful
3=====================3=====================
44
50.19.5 (2012-03-13)
6===================
7
8Fixed bug 953587: add a dict marshaller so that exported method parameters
9can be of type dict.
10
50.19.4 (2011-10-11)110.19.4 (2011-10-11)
6===================12===================
713
814
=== modified file 'src/lazr/restful/configure.zcml'
--- src/lazr/restful/configure.zcml 2011-03-22 14:42:59 +0000
+++ src/lazr/restful/configure.zcml 2012-03-13 23:50:24 +0000
@@ -272,6 +272,13 @@
272 />272 />
273273
274 <adapter274 <adapter
275 for="zope.schema.interfaces.IDict
276 zope.publisher.interfaces.http.IHTTPRequest"
277 provides="lazr.restful.interfaces.IFieldMarshaller"
278 factory="lazr.restful.marshallers.DictFieldMarshaller"
279 />
280
281 <adapter
275 for="lazr.restful.interfaces.ICollectionField282 for="lazr.restful.interfaces.ICollectionField
276 zope.publisher.interfaces.http.IHTTPRequest"283 zope.publisher.interfaces.http.IHTTPRequest"
277 provides="lazr.restful.interfaces.IFieldMarshaller"284 provides="lazr.restful.interfaces.IFieldMarshaller"
278285
=== modified file 'src/lazr/restful/docs/webservice-marshallers.txt'
--- src/lazr/restful/docs/webservice-marshallers.txt 2010-10-05 15:49:55 +0000
+++ src/lazr/restful/docs/webservice-marshallers.txt 2012-03-13 23:50:24 +0000
@@ -700,9 +700,10 @@
700collection of objects associated with some other object. The generic700collection of objects associated with some other object. The generic
701collection marshaller will take care of marshalling to the proper701collection marshaller will take care of marshalling to the proper
702collection type, and of marshalling the individual items using the702collection type, and of marshalling the individual items using the
703marshaller for its value_type.703marshaller for its value_type. Dictionaries may specify separate
704marshallers for their keys and values.
704705
705 >>> from zope.schema import List, Tuple, Set706 >>> from zope.schema import Dict, List, Tuple, Set
706 >>> list_of_strings_field = List(value_type=Text())707 >>> list_of_strings_field = List(value_type=Text())
707 >>> from lazr.restful.example.base.interfaces import Cuisine708 >>> from lazr.restful.example.base.interfaces import Cuisine
708 >>> tuple_of_ints_field = Tuple(value_type=Int())709 >>> tuple_of_ints_field = Tuple(value_type=Int())
@@ -710,6 +711,9 @@
710 ... value_type=Choice(vocabulary=Cuisine))711 ... value_type=Choice(vocabulary=Cuisine))
711 >>> set_of_choices_field = Set(712 >>> set_of_choices_field = Set(
712 ... value_type=Choice(vocabulary=Cuisine)).bind(None)713 ... value_type=Choice(vocabulary=Cuisine)).bind(None)
714 >>> dict_of_choices_field = Dict(
715 ... key_type=Text(),
716 ... value_type=Choice(vocabulary=Cuisine))
713717
714 >>> list_marshaller = getMultiAdapter(718 >>> list_marshaller = getMultiAdapter(
715 ... (list_of_strings_field, request), IFieldMarshaller)719 ... (list_of_strings_field, request), IFieldMarshaller)
@@ -731,9 +735,22 @@
731 >>> verifyObject(IFieldMarshaller, set_marshaller)735 >>> verifyObject(IFieldMarshaller, set_marshaller)
732 True736 True
733737
734The only JSON representation for the collection itself is a list, since738 >>> dict_marshaller = getMultiAdapter(
735that's the only sequence type available in JSON. Anything else will739 ... (dict_of_choices_field, request), IFieldMarshaller)
736raise a ValueError.740 >>> verifyObject(IFieldMarshaller, dict_marshaller)
741 True
742 >>> dict_key_marshaller = getMultiAdapter(
743 ... (dict_of_choices_field.key_type, request), IFieldMarshaller)
744 >>> verifyObject(IFieldMarshaller, dict_key_marshaller)
745 True
746 >>> dict_value_marshaller = getMultiAdapter(
747 ... (dict_of_choices_field.value_type, request), IFieldMarshaller)
748 >>> verifyObject(IFieldMarshaller, dict_value_marshaller)
749 True
750
751For sequences, the only JSON representation for the collection itself is a
752list, since that's the only sequence type available in JSON. Anything else
753will raise a ValueError.
737754
738 >>> list_marshaller.marshall_from_json_data([u"Test"])755 >>> list_marshaller.marshall_from_json_data([u"Test"])
739 [u'Test']756 [u'Test']
@@ -743,11 +760,40 @@
743 ...760 ...
744 ValueError: got 'unicode', expected list: u'Test'761 ValueError: got 'unicode', expected list: u'Test'
745762
763For dicts, we support marshalling from sequences of (name, value) pairs as
764well as from dicts or even strings which are interpreted as single element
765lists.
766
767 >>> dict_marshaller.marshall_from_json_data({u"foo": u"Vegetarian"})
768 {u'foo': <Item Cuisine.VEGETARIAN, ...>}
769
770 >>> dict_marshaller.marshall_from_json_data([(u"foo", u"Vegetarian")])
771 {u'foo': <Item Cuisine.VEGETARIAN, ...>}
772
773 >>> dict_marshaller.marshall_from_request(u"foo,Vegetarian")
774 {u'foo': <Item Cuisine.VEGETARIAN, ...>}
775
776If we attempt to marshall something other than one of the above data formats,
777a ValueError will be raised.
778
779 >>> dict_marshaller.marshall_from_json_data(u"Test")
780 Traceback (most recent call last):
781 ...
782 ValueError: got 'unicode', expected dict: u'Test'
783
784 >>> dict_marshaller.marshall_from_request(u"Test")
785 Traceback (most recent call last):
786 ...
787 ValueError: got '[u'Test']', list of name,value pairs
788
746None is passed through though.789None is passed through though.
747790
748 >>> print list_marshaller.marshall_from_json_data(None)791 >>> print list_marshaller.marshall_from_json_data(None)
749 None792 None
750793
794 >>> print dict_marshaller.marshall_from_json_data(None)
795 None
796
751ValueError is also raised if one of the value in the list doesn't797ValueError is also raised if one of the value in the list doesn't
752validate against the more specific marshaller.798validate against the more specific marshaller.
753799
@@ -760,6 +806,18 @@
760 ... [u'Vegetarian', u'NoSuchChoice'])806 ... [u'Vegetarian', u'NoSuchChoice'])
761 ValueError: Invalid value "NoSuchChoice"...807 ValueError: Invalid value "NoSuchChoice"...
762808
809ValueError is also raised if one of the keys or values in the dict doesn't
810validate against the more specific marshaller.
811
812 >>> dict_marshaller.marshall_from_json_data({1: u"Vegetarian"})
813 Traceback (most recent call last):
814 ...
815 ValueError: got 'int', expected unicode: 1
816
817 >>> show_ValueError(dict_marshaller.marshall_from_request,
818 ... {u'foo': u'NoSuchChoice'})
819 ValueError: Invalid value "NoSuchChoice"...
820
763The return type is correctly typed to the concrete collection.821The return type is correctly typed to the concrete collection.
764822
765 >>> tuple_marshaller.marshall_from_json_data([1, 2, 3])823 >>> tuple_marshaller.marshall_from_json_data([1, 2, 3])
@@ -776,16 +834,28 @@
776 >>> [item.title for item in result]834 >>> [item.title for item in result]
777 ['Vegetarian', 'General']835 ['Vegetarian', 'General']
778836
837 >>> marshalled_dict = dict_marshaller.marshall_from_json_data(
838 ... {u'foo': u'Vegetarian', u'bar': u'General'})
839 >>> type(marshalled_dict)
840 <type 'dict'>
841 >>> sorted(
842 ... [(key, value.title) for key, value in marshalled_dict.items()])
843 [(u'bar', 'General'), (u'foo', 'Vegetarian')]
779844
780When coming from the request, either a list or a JSON-encoded845When coming from the request, either a list or a JSON-encoded
781representation is accepted. The normal request rules for the846representation is accepted. The normal request rules for the
782underlying type are then followed.847underlying type are then followed. When marshalling dicts, the
848list elements are name,value strings which are pulled apart and
849used to populate the dict.
783850
784 >>> list_marshaller.marshall_from_request([u'1', u'2'])851 >>> list_marshaller.marshall_from_request([u'1', u'2'])
785 [u'1', u'2']852 [u'1', u'2']
786 >>> list_marshaller.marshall_from_request('["1", "2"]')853 >>> list_marshaller.marshall_from_request('["1", "2"]')
787 [u'1', u'2']854 [u'1', u'2']
788855
856 >>> dict_marshaller.marshall_from_request('["foo,Vegetarian"]')
857 {u'foo': <Item Cuisine.VEGETARIAN, ...>}
858
789 >>> tuple_marshaller.marshall_from_request([u'1', u'2'])859 >>> tuple_marshaller.marshall_from_request([u'1', u'2'])
790 (1, 2)860 (1, 2)
791861
@@ -794,6 +864,9 @@
794 >>> print list_marshaller.marshall_from_request('null')864 >>> print list_marshaller.marshall_from_request('null')
795 None865 None
796866
867 >>> print dict_marshaller.marshall_from_request('null')
868 None
869
797Also, as a convenience for web client, so that they don't have to JSON870Also, as a convenience for web client, so that they don't have to JSON
798encode single-element list, non-list value are promoted into a871encode single-element list, non-list value are promoted into a
799single-element list.872single-element list.
@@ -810,6 +883,10 @@
810 >>> sorted(set_marshaller.unmarshall(None, marshalled_set))883 >>> sorted(set_marshaller.unmarshall(None, marshalled_set))
811 ['Dessert', 'Vegetarian']884 ['Dessert', 'Vegetarian']
812885
886 >>> unmarshalled = dict_marshaller.unmarshall(None, marshalled_dict)
887 >>> sorted(unmarshalled.iteritems())
888 [(u'bar', 'General'), (u'foo', 'Vegetarian')]
889
813CollectionField890CollectionField
814---------------891---------------
815892
816893
=== modified file 'src/lazr/restful/marshallers.py'
--- src/lazr/restful/marshallers.py 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/marshallers.py 2012-03-13 23:50:24 +0000
@@ -9,6 +9,7 @@
9 'BytesFieldMarshaller',9 'BytesFieldMarshaller',
10 'CollectionFieldMarshaller',10 'CollectionFieldMarshaller',
11 'DateTimeFieldMarshaller',11 'DateTimeFieldMarshaller',
12 'DictFieldMarshaller',
12 'FloatFieldMarshaller',13 'FloatFieldMarshaller',
13 'IntFieldMarshaller',14 'IntFieldMarshaller',
14 'ObjectLookupFieldMarshaller',15 'ObjectLookupFieldMarshaller',
@@ -112,7 +113,7 @@
112 path_parts = [urllib.unquote(part) for part in path.split('/')]113 path_parts = [urllib.unquote(part) for part in path.split('/')]
113 path_parts.pop(0)114 path_parts.pop(0)
114 path_parts.reverse()115 path_parts.reverse()
115 request = config.createRequest(StringIO(), {'PATH_INFO' : path})116 request = config.createRequest(StringIO(), {'PATH_INFO': path})
116 request.setTraversalStack(path_parts)117 request.setTraversalStack(path_parts)
117 root = request.publication.getApplication(self.request)118 root = request.publication.getApplication(self.request)
118 return request.traverse(root)119 return request.traverse(root)
@@ -125,7 +126,6 @@
125 """126 """
126 implements(IFieldMarshaller)127 implements(IFieldMarshaller)
127128
128
129 # Set this to type or tuple of types that the JSON value must be of.129 # Set this to type or tuple of types that the JSON value must be of.
130 _type = None130 _type = None
131131
@@ -153,7 +153,7 @@
153 if value != '':153 if value != '':
154 try:154 try:
155 v = value155 v = value
156 if isinstance (v, str):156 if isinstance(v, str):
157 v = v.decode('utf8') # assume utf8157 v = v.decode('utf8') # assume utf8
158 # XXX gary 2009-03-28158 # XXX gary 2009-03-28
159 # The use of the enclosing brackets is a hack to work around159 # The use of the enclosing brackets is a hack to work around
@@ -327,7 +327,7 @@
327 """Describe all values, not just the selected value."""327 """Describe all values, not just the selected value."""
328 unmarshalled = []328 unmarshalled = []
329 for item in self.field.vocabulary:329 for item in self.field.vocabulary:
330 item_dict = {'token' : item.token, 'title' : item.title}330 item_dict = {'token': item.token, 'title': item.title}
331 if value.title == item.title:331 if value.title == item.title:
332 item_dict['selected'] = True332 item_dict['selected'] = True
333 unmarshalled.append(item_dict)333 unmarshalled.append(item_dict)
@@ -462,6 +462,84 @@
462 return set462 return set
463463
464464
465class DictFieldMarshaller(SimpleFieldMarshaller):
466 """A marshaller for Dicts.
467
468 It looks up the marshallers for its (key-type, value-type), to handle its
469 contained name/value pairs.
470 """
471 _type = dict
472 _type_error_message = 'not a dict: %r'
473
474 def __init__(self, field, request):
475 """See `SimpleFieldMarshaller`.
476
477 This also looks for the appropriate marshaller for key_type and
478 value_type.
479 """
480 super(DictFieldMarshaller, self).__init__(
481 field, request)
482 self.key_marshaller = getMultiAdapter(
483 (field.key_type, request), IFieldMarshaller)
484 self.value_marshaller = getMultiAdapter(
485 (field.value_type, request), IFieldMarshaller)
486
487 def _marshall_from_request(self, value):
488 """See `IFieldMarshaller`.
489
490 All items in the dict are marshalled using the appropriate
491 `IFieldMarshaller` for the key_type, value_type.
492
493 When coming in via HTML request parameters, the dict is sent as a list
494 of comma separated "key,value" strings.
495 If the value isn't a list or dict, transform it into a one-element list.
496 That allows web client to submit one-element list of strings
497 without having to JSON-encode it.
498 """
499 if not isinstance(value, list) and not isinstance(value, dict):
500 value = [value]
501 if isinstance(value, list):
502 try:
503 value = dict(nv.split(',', 2) for nv in value)
504 except ValueError:
505 raise ValueError(
506 u"got '%s', list of name,value pairs" % value)
507 return dict(
508 (self.key_marshaller.marshall_from_json_data(key),
509 self.value_marshaller.marshall_from_json_data(val))
510 for key, val in value.items())
511
512 def _marshall_from_json_data(self, value):
513 """See `SimpleFieldMarshaller`.
514
515 Marshall the items in the dict using the appropriate
516 (key,value) marshallers.
517
518 The incoming value may be a list of name,value pairs in which case
519 we convert to a dict first.
520 """
521 if isinstance(value, list):
522 value = dict(value)
523 # Call super to check for errors and raise a ValueError if necessary.
524 value = super(
525 DictFieldMarshaller, self)._marshall_from_json_data(value)
526 return dict(
527 (self.key_marshaller.marshall_from_json_data(key),
528 self.value_marshaller.marshall_from_json_data(val))
529 for key, val in value.items())
530
531 def unmarshall(self, entry, value):
532 """See `SimpleFieldMarshaller`.
533
534 The value is unmarshalled into a dict and all its items are
535 unmarshalled using the appropriate FieldMarshaller.
536 """
537 return dict(
538 (self.key_marshaller.unmarshall(entry, key),
539 self.value_marshaller.unmarshall(entry, val))
540 for key, val in value.items())
541
542
465class CollectionFieldMarshaller(SimpleFieldMarshaller):543class CollectionFieldMarshaller(SimpleFieldMarshaller):
466 """A marshaller for collection fields."""544 """A marshaller for collection fields."""
467 implements(IUnmarshallingDoesntNeedValue)545 implements(IUnmarshallingDoesntNeedValue)
@@ -569,4 +647,3 @@
569 # to the object underlying a resource, we need to strip its647 # to the object underlying a resource, we need to strip its
570 # security proxy.648 # security proxy.
571 return removeSecurityProxy(resource).context649 return removeSecurityProxy(resource).context
572
573650
=== modified file 'src/lazr/restful/version.txt'
--- src/lazr/restful/version.txt 2011-10-11 01:45:29 +0000
+++ src/lazr/restful/version.txt 2012-03-13 23:50:24 +0000
@@ -1,1 +1,1 @@
10.19.410.19.5

Subscribers

People subscribed via source and target branches