source: pyson/test/DeserializerTest.py@ 1283

Last change on this file since 1283 was 1283, checked in by wouter, 3 weeks ago

#401 added Class (de)serialization support

File size: 11.3 KB
Line 
1from __future__ import annotations
2from abc import ABC
3from datetime import datetime
4from decimal import Decimal
5import json
6import re
7import sys, traceback
8from typing import Dict, List, Set, Any, Union, Optional, Collection, Type
9import unittest
10from uuid import uuid4, UUID
11
12from pyson.Deserializer import Deserializer
13from pyson.Serializer import Serializer
14from pyson.JsonSerialize import JsonSerialize
15from pyson.JsonDeserialize import JsonDeserialize
16from pyson.JsonGetter import JsonGetter
17from pyson.JsonSubTypes import JsonSubTypes
18from pyson.JsonTypeInfo import Id, As
19from pyson.JsonTypeInfo import JsonTypeInfo
20from pyson.JsonValue import JsonValue
21from pyson.ObjectMapper import ObjectMapper
22from pickle import NONE
23from test.MyDeserializer import ValueDeserializer2
24from cgitb import reset
25
26class SuperSimple:
27 def __init__(self, a:Optional[int]):
28 self.a=a
29 def __eq__(self, other):
30 return isinstance(other, self.__class__) and \
31 self.a==other.a
32 def getA(self):
33 return self.a
34 def __repr__(self):
35 return "SuperSimple::"+str(self.a)
36 def __hash__(self):
37 return hash(self.a)
38
39
40
41# deserializer DROPS FIRST CHAR from string and assumes rest is an int.
42# Then returns Simple(int)
43class ValueDeserializer(Deserializer):
44 def __hash__(self):
45 return hash(self.geta())
46 def deserialize(self, data:object, clas: object)-> object:
47 if type(data)!=str:
48 raise ValueError("Expected str starting with '$', got "+str(data))
49 return Simple(int(data[1:]))
50
51# serializes Simple object, just prefixing its value (as string) with "$"
52class ValueSerializer(Serializer):
53 def serialize(self, obj:object)-> object:
54 if not isinstance(obj, Simple):
55 raise ValueError("Expected Dimple object")
56 return "$" + str(obj.geta())
57
58class Basic:
59 def __init__(self, v:float):
60 self._v=v
61 def __eq__(self, other):
62 return isinstance(other, self.__class__) and \
63 self._v==other._v
64 def getV(self):
65 return self._v
66 def __repr__(self):
67 return "Basic:"+str(self._v)
68 def __hash__(self):
69 return hash(self._v)
70
71@JsonDeserialize(ValueDeserializer)
72@JsonSerialize(ValueSerializer)
73class Simple:
74 def __init__(self, a:int):
75 self._a=a
76 def geta(self)->int:
77 return self._a
78 def __eq__(self, other):
79 return isinstance(other, self.__class__) and \
80 self._a==other._a
81 def __repr__(self):
82 return "Simple:"+str(self._a)
83 def __hash__(self):
84 return hash(self._a)
85
86
87
88@JsonDeserialize(ValueDeserializer2)
89class Simple2:
90 def __init__(self, a:int):
91 self._a=a
92 def geta(self)->int:
93 return self._a
94 def __eq__(self, other):
95 return isinstance(other, self.__class__) and \
96 self._a==other._a
97 def __repr__(self):
98 return self._name+","+str(self._a)
99
100#None cancels out the existing deserializer.
101# parsing with str should now fail.
102@JsonDeserialize(None)
103class Simple3 (Simple):
104 pass
105
106
107class MyList:
108 def __init__(self, data: List[Simple] ):
109 self.data=data
110 def getData(self)->List[Simple]:
111 return self.data
112
113class BasicDict:
114 def __init__(self, data: Dict[Basic,Basic] ):
115 self.data=data
116 def getData(self)->Dict[Basic,Basic]:
117 return self.data
118 def __repr__(self):
119 return "BasicDict:"+str(self.data)
120 def __eq__(self, other):
121 return isinstance(other, self.__class__) and \
122 self.data==other.data
123
124
125class SimpleDict:
126 def __init__(self, data: Dict[Simple,Simple] ):
127 self.data=data
128 def getData(self)->Dict[Simple,Simple]:
129 return self.data
130 def __eq__(self, other):
131 return isinstance(other, self.__class__) and \
132 self.data==other.data
133 def __repr__(self):
134 return "SimpleDict:"+str(self.data)
135
136
137class MyOptionalString:
138 def __init__(self, s:Optional[str]):
139 self.data:Optional[str] = s
140 def getData(self)->Optional[str]:
141 return self.data
142 def __eq__(self, other):
143 return isinstance(other, self.__class__) and \
144 self.data==other.data
145 def __repr__(self):
146 return "MyOptionalString:"+str(self.data)
147
148
149@JsonSubTypes(['test.DeserializerTest.A','test.DeserializerTest.B','test.DeserializerTest.C'])
150@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
151class Root:
152 def __init__(self, data: int ):
153 self.data:int=data
154 def getData(self)->int:
155 return self.data
156 def __eq__(self, other):
157 return isinstance(other, self.__class__) and \
158 self.data==other.data
159 def __repr__(self):
160 return type(self).__name__ + "/" + str(self.data)
161 def __hash__(self):
162 return 0
163
164
165class A(Root):
166 pass
167
168class B(Root):
169 pass
170
171class C(Root):
172 def __init__(self, data:int, name: str):
173 super().__init__(data)
174 self.name = name
175 def __eq__(self, other):
176 return isinstance(other, self.__class__) and \
177 self.data==other.data and self.name==other.name
178 def __repr__(self):
179 return super().__repr__()+","+self.name
180
181
182
183
184class BaseValueDeserializer (Deserializer):
185 def deserialize(self, data:object, clas: object) -> 'BaseValue':
186 if isinstance(data,str):
187 return BaseDiscreteValue(data)
188 raise ValueError("Expected number or double quoted string but found " + str(data)
189 + " of type " + str(type(data)))
190
191@JsonDeserialize(using=BaseValueDeserializer)
192class BaseValue(ABC):
193 def __init__(self, value):
194 self._value = value;
195
196 @JsonValue()
197 def getValue(self) -> str:
198 return self._value;
199
200#disable parent deserializer. This is essential #331
201#@JsonDeserialize(using=None)
202class BaseDiscreteValue(BaseValue):
203 def __init__(self, value:str):
204 self.__value=value
205 @JsonValue()
206 def getValue(self)->str:
207 return self.__value
208
209
210class DeserializerTest(unittest.TestCase):
211 '''
212 Test a lot of back-and-forth cases.
213 FIXME Can we make this a parameterized test?
214 '''
215 pyson=ObjectMapper()
216 mixedList:List[Root] = [A(1), B(1)]
217 mixedlistjson:List = [{'A': {'data': 1}}, {'B': {'data': 1}}]
218
219 mixedDict:Dict[Root,int] = {A(1):1, B(1):2}
220 mixedDictJson:dict = {'{"A": {"data": 1}}': 1, '{"B": {"data": 1}}': 2}
221 MyC:C = C(3, "MyC")
222
223 def testDeserialize(self):
224 objson= "$12"
225 self.assertEqual(Simple(12), self.pyson.parse(objson, Simple))
226
227 def testExternalDeserialize2(self):
228 objson= "$13"
229 self.assertEqual(13, self.pyson.parse(objson, Simple2))
230
231
232 def testExternalDeserialize3(self):
233 objson= "$13"
234 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, Simple3))
235
236 def testDeserializeMyList(self):
237 print(self.pyson.toJson(MyList([12,13])))
238
239 # the json we provide is NOT the proper json but a STRING.
240 # This triggers a fallback mechanism that tries to parse the string as json.
241 objson={"data": ["$12", "$13"]}
242 res = self.pyson.parse(objson, MyList)
243 print(res)
244 self.assertEqual([Simple(12),Simple(13)], res.data)
245
246 def testDeserializeCollection(self):
247 objson=[1,2,3]
248 # this checks that pyson can parse a list as Collection.
249 # The result is a simple list like in java/jackson.
250 # Collection without typing info means Collectino[Any]
251 res = self.pyson.parse(objson, Collection)
252 print(res)
253 self.assertEqual([1,2,3], res)
254
255
256 def testSerializeBasicDict(self):
257 '''
258 Basic object keys. Special (de)serializer should kick in #190
259 '''
260 d= BasicDict( { Basic(1.):Basic(2.), Basic(3.): Basic(4.) } )
261 objson={"data": {"{\"v\": 1.0}": {"v": 2.0}, "{\"v\": 3.0}": {"v": 4.0}}}
262 dump=json.dumps(self.pyson.toJson(d));
263 print(dump)
264 # self.assertEqual(objson, dump);
265 res=self.pyson.parse(objson, BasicDict)
266 print("res="+str(res))
267 self.assertEqual(d, res)
268
269
270 def testSerializeSimpleDictCustomSerializer(self):
271 d= SimpleDict( { Simple(1):Simple(2), Simple(3): Simple(4) } )
272
273 # The keys need extra quotes, they are deserialied by
274 # our special deserializer that parses the string as json,
275 # and json requires double quotes around its strings.
276 objson = {"data": {'"$1"': "$2", '"$3"': "$4"}}
277
278 obj=self.pyson.toJson(d)
279 print(json.dumps(obj))
280 self.assertEqual(objson, obj)
281
282 res = self.pyson.parse(objson, SimpleDict)
283 print("res="+str(res))
284 self.assertEqual(d, res)
285
286 def testDeserializeOptString(self):
287 objson:dict={'s':None}
288 res = self.pyson.parse(objson, MyOptionalString)
289 print(res)
290 self.assertEqual(MyOptionalString(None), res)
291
292 objson={'s':"something"}
293 res = self.pyson.parse(objson, MyOptionalString)
294 print(res)
295 self.assertEqual(MyOptionalString("something"), res)
296
297 def testSerializeMixedList(self):
298 # see #296
299 res=self.pyson.toJson(self.mixedList)
300 print(res)
301 self.assertEqual(self.mixedlistjson, res)
302
303 def testDeserializeMixedList(self):
304 # see #296
305 res=self.pyson.parse(self.mixedlistjson, List[Root])
306 print(res)
307 self.assertEqual(self.mixedList,res)
308
309 def testDeserializeBadList(self):
310 # see #298. This SHOULD fail because B is not subtype of A
311 self.assertRaises(ValueError, lambda:self.pyson.parse(self.mixedlistjson, List[A]))
312
313
314 def testSerializeMixedDict(self):
315 # see #296
316 res=self.pyson.toJson(self.mixedDict)
317 print(res)
318 self.assertEqual(self.mixedDictJson, res)
319
320 def testDeserializeMixedDict(self):
321 # see #296
322 print("testDeserializeMixedDict")
323 res=self.pyson.parse(self.mixedDictJson, Dict[Root,int])
324 print(res)
325 self.assertEqual(self.mixedDict,res)
326
327 def testDeserializeC(self):
328 # see #298. Sub-class should not expect
329 res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, Root)
330 print(reset)
331 self.assertEqual(self.MyC, res)
332 # even if you serialize as C, you still need the 'C' wrapper
333 res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, C)
334 self.assertEqual(self.MyC, res)
335
336 def testDeserializeBaseValue(self):
337 # see #331
338 res=self.pyson.parse({'yes': 1, 'no': 0}, Dict[BaseDiscreteValue, int])
339 print(res)
340
341
342 def testDeserializeSuperSimple(self):
343 res=self.pyson.parse({'a':1 } , SuperSimple)
344 print(res)
345 self.assertEqual(SuperSimple(1), res)
346
347
348 def testDeserializeMissingValue(self):
349 res=self.pyson.parse({ } , SuperSimple) # missing value for a
350 print(res)
351 self.assertEqual(SuperSimple(None), res)
352
353 def testDeserializeStandardDict(self):
354 val:Dict = self.pyson.parse(json.loads("{\"a\":0.3,\"b\":{\"x\":3},\"c\":[1,2,3]}"),Dict)
355
356
357 def testDeserializeClass(self):
358 clazz:Type[Root] = self.pyson.parse("test.DeserializerTest.C", Type[Root])
359 self.assertEquals(C, clazz)
360
361 def testSerializeClass(self):
362 js = self.pyson.toJson(C)
363 print("serialized Class<C>:"+json.dumps(js))
364 self.assertEquals("test.DeserializerTest.C", js)
365
366
367
Note: See TracBrowser for help on using the repository browser.