source: pyson/test/DeserializerTest.py@ 1364

Last change on this file since 1364 was 1364, checked in by wouter, 3 days ago

#422 added test showing issue when combining @JsonValue and @JsonTypeInfo.

File size: 12.8 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
210
211@JsonSubTypes(['test.DeserializerTest.SubClass'])
212@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
213class RootClass(SuperSimple):
214 pass
215
216@JsonSubTypes(['test.DeserializerTest.SubSubClass'])
217class SubClass(RootClass):
218 pass
219
220# INDIRECTLY inherits from RootClass.
221class SubSubClass(SubClass):
222 pass
223
224
225@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
226class BothTypeInfoAndValue:
227 def __init__(self, val:str):
228 self.__value=val
229 @JsonValue()
230 def getVal(self)->str:
231 return self.__value
232
233class DeserializerTest(unittest.TestCase):
234 '''
235 Test a lot of back-and-forth cases.
236 FIXME Can we make this a parameterized test?
237 '''
238 pyson=ObjectMapper()
239 mixedList:List[Root] = [A(1), B(1)]
240 mixedlistjson:List = [{'A': {'data': 1}}, {'B': {'data': 1}}]
241
242 mixedDict:Dict[Root,int] = {A(1):1, B(1):2}
243 mixedDictJson:dict = {'{"A": {"data": 1}}': 1, '{"B": {"data": 1}}': 2}
244 MyC:C = C(3, "MyC")
245
246 def testDeserialize(self):
247 objson= "$12"
248 self.assertEqual(Simple(12), self.pyson.parse(objson, Simple))
249
250 def testExternalDeserialize2(self):
251 objson= "$13"
252 self.assertEqual(13, self.pyson.parse(objson, Simple2))
253
254
255 def testExternalDeserialize3(self):
256 objson= "$13"
257 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, Simple3))
258
259 def testDeserializeMyList(self):
260 print(self.pyson.toJson(MyList([12,13])))
261
262 # the json we provide is NOT the proper json but a STRING.
263 # This triggers a fallback mechanism that tries to parse the string as json.
264 objson={"data": ["$12", "$13"]}
265 res = self.pyson.parse(objson, MyList)
266 print(res)
267 self.assertEqual([Simple(12),Simple(13)], res.data)
268
269 def testDeserializeCollection(self):
270 objson=[1,2,3]
271 # this checks that pyson can parse a list as Collection.
272 # The result is a simple list like in java/jackson.
273 # Collection without typing info means Collectino[Any]
274 res = self.pyson.parse(objson, Collection)
275 print(res)
276 self.assertEqual([1,2,3], res)
277
278
279 def testSerializeBasicDict(self):
280 '''
281 Basic object keys. Special (de)serializer should kick in #190
282 '''
283 d= BasicDict( { Basic(1.):Basic(2.), Basic(3.): Basic(4.) } )
284 objson={"data": {"{\"v\": 1.0}": {"v": 2.0}, "{\"v\": 3.0}": {"v": 4.0}}}
285 dump=json.dumps(self.pyson.toJson(d));
286 print(dump)
287 # self.assertEqual(objson, dump);
288 res=self.pyson.parse(objson, BasicDict)
289 print("res="+str(res))
290 self.assertEqual(d, res)
291
292
293 def testSerializeSimpleDictCustomSerializer(self):
294 d= SimpleDict( { Simple(1):Simple(2), Simple(3): Simple(4) } )
295
296 # The keys need extra quotes, they are deserialied by
297 # our special deserializer that parses the string as json,
298 # and json requires double quotes around its strings.
299 objson = {"data": {'"$1"': "$2", '"$3"': "$4"}}
300
301 obj=self.pyson.toJson(d)
302 print(json.dumps(obj))
303 self.assertEqual(objson, obj)
304
305 res = self.pyson.parse(objson, SimpleDict)
306 print("res="+str(res))
307 self.assertEqual(d, res)
308
309 def testDeserializeOptString(self):
310 objson:dict={'s':None}
311 res = self.pyson.parse(objson, MyOptionalString)
312 print(res)
313 self.assertEqual(MyOptionalString(None), res)
314
315 objson={'s':"something"}
316 res = self.pyson.parse(objson, MyOptionalString)
317 print(res)
318 self.assertEqual(MyOptionalString("something"), res)
319
320 def testSerializeMixedList(self):
321 # see #296
322 res=self.pyson.toJson(self.mixedList)
323 print(res)
324 self.assertEqual(self.mixedlistjson, res)
325
326 def testDeserializeMixedList(self):
327 # see #296
328 res=self.pyson.parse(self.mixedlistjson, List[Root])
329 print(res)
330 self.assertEqual(self.mixedList,res)
331
332 def testDeserializeBadList(self):
333 # see #298. This SHOULD fail because B is not subtype of A
334 self.assertRaises(ValueError, lambda:self.pyson.parse(self.mixedlistjson, List[A]))
335
336
337 def testSerializeMixedDict(self):
338 # see #296
339 res=self.pyson.toJson(self.mixedDict)
340 print(res)
341 self.assertEqual(self.mixedDictJson, res)
342
343 def testDeserializeMixedDict(self):
344 # see #296
345 print("testDeserializeMixedDict")
346 res=self.pyson.parse(self.mixedDictJson, Dict[Root,int])
347 print(res)
348 self.assertEqual(self.mixedDict,res)
349
350 def testDeserializeC(self):
351 # see #298. Sub-class should not expect
352 res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, Root)
353 print(reset)
354 self.assertEqual(self.MyC, res)
355 # even if you serialize as C, you still need the 'C' wrapper
356 res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, C)
357 self.assertEqual(self.MyC, res)
358
359 def testDeserializeBaseValue(self):
360 # see #331
361 res=self.pyson.parse({'yes': 1, 'no': 0}, Dict[BaseDiscreteValue, int])
362 print(res)
363
364
365 def testDeserializeSuperSimple(self):
366 res=self.pyson.parse({'a':1 } , SuperSimple)
367 print(res)
368 self.assertEqual(SuperSimple(1), res)
369
370
371 def testDeserializeMissingValue(self):
372 res=self.pyson.parse({ } , SuperSimple) # missing value for a
373 print(res)
374 self.assertEqual(SuperSimple(None), res)
375
376 def testDeserializeStandardDict(self):
377 val:Dict = self.pyson.parse(json.loads("{\"a\":0.3,\"b\":{\"x\":3},\"c\":[1,2,3]}"),Dict)
378
379
380 def testDeserializeClass(self):
381 clazz:Type[Root] = self.pyson.parse("test.DeserializerTest$C", Type[Root])
382 self.assertEquals(C, clazz)
383
384 def testSerializeClass(self):
385 js = self.pyson.toJson(C)
386 print("serialized Class<C>:"+json.dumps(js))
387 self.assertEquals("test.DeserializerTest$C", js)
388
389
390 def testDeserializeListOfClass(self):
391 clazzlist:List[Type[Root]] = self.pyson.parse(["test.DeserializerTest$C","test.DeserializerTest$A"], List[Type[Root]])
392 self.assertEquals([C,A], clazzlist)
393
394
395 def testSerializeSubSubClass(self):
396 js = self.pyson.toJson(SubSubClass(22))
397 print("serialized SuperSimple:"+json.dumps(js))
398 self.assertEquals({'SubSubClass': {'a': 22}}, js)
399
400
401 def testDeserializeSubSubClass(self):
402 js=self.pyson.parse({'SubSubClass': {'a': 22}}, RootClass)
403 self.assertEquals(SubSubClass(22), js)
404
405
406 def testSerializeBothTypeInfoAndValue(self):
407 js = self.pyson.toJson(BothTypeInfoAndValue("ok"))
408 print("serialized BothTypeInfoAndValue:"+json.dumps(js))
409 self.assertEquals({'BothTypeInfoAndValue': "ok"}, js)
410
411 def testDeserializeBothTypeInfoAndValue(self):
412 js = self.pyson.parse({'BothTypeInfoAndValue': "ok"}, BothTypeInfoAndValue)
413 self.assertEquals(BothTypeInfoAndValue("ok"), js)
414
415
Note: See TracBrowser for help on using the repository browser.