source: pyson/test/DeserializerTest.py@ 1359

Last change on this file since 1359 was 1359, checked in by wouter, 5 hours ago

#420 added test showing JsonSubTypes issue

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