source: pyson/test/ObjectMapperTest.py@ 192

Last change on this file since 192 was 192, checked in by wouter, 3 years ago

#82 fixed compatibility with python 3.9. pyson now works both with 3.8 and 3.9.

File size: 19.2 KB
Line 
1
2from datetime import datetime
3import sys, traceback
4
5from uri import URI
6
7from abc import ABC
8from decimal import Decimal
9import json
10from pyson.Deserializer import Deserializer
11from pyson.JsonDeserialize import JsonDeserialize
12from pyson.JsonGetter import JsonGetter
13from pyson.JsonSubTypes import JsonSubTypes
14from pyson.JsonTypeInfo import Id, As
15from pyson.JsonTypeInfo import JsonTypeInfo
16from pyson.JsonValue import JsonValue
17from pyson.ObjectMapper import ObjectMapper
18import re
19from typing import Dict, List, Set, Any, Union
20import unittest
21from uuid import uuid4, UUID
22
23
24class Props:
25 '''
26 compound class with properties, used for testing
27 '''
28 def __init__(self, age:int, name:str):
29 if age<0:
30 raise ValueError("age must be >0, got "+str(age))
31 self._age=age
32 self._name=name;
33 def __str__(self):
34 return self._name+","+str(self._age)
35 def getage(self):
36 return self._age
37 def getname(self):
38 return self._name
39 def __eq__(self, other):
40 return isinstance(other, self.__class__) and \
41 self._name==other._name and self._age==other._age
42
43@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
44class Simple:
45 def __init__(self, a:int):
46 self._a=a
47 def geta(self)->int:
48 return self._a
49 def __eq__(self, other):
50 return isinstance(other, self.__class__) and \
51 self._a==other._a
52 def __str__(self):
53 return self._name+","+str(self._a)
54
55
56class SimpleWithHash(Simple):
57 def __hash__(self):
58 return hash(self.geta())
59
60# define abstract root class
61# These need to be reachable globally for reference
62@JsonSubTypes(["test.ObjectMapperTest.Bear"])
63@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
64class Animal:
65 pass
66
67
68class Bear(Animal):
69 def __init__(self, props:Props):
70 self._props=props
71
72 def __str__(self):
73 return "Bear["+str(self._props)+"]"
74
75 def getprops(self):
76 return self._props
77 def __eq__(self, other):
78 return isinstance(other, self.__class__) and \
79 self._props==other._props
80
81
82
83# A wrongly configured type, you must add
84#@JsonSubTypes(["test.ObjectMapperTest.BadSubclass"])
85class BadSuperclassMissingTypeInfo:
86 pass
87
88class BadSubclass(BadSuperclassMissingTypeInfo):
89 def __init__(self, a:int):
90 self._a=a
91 def geta(self)->int:
92 return self._a
93 def __eq__(self, other):
94 return isinstance(other, self.__class__) and \
95 self._a==other._a
96 def __str__(self):
97 return self._name+","+str(self._a)
98
99#module instead of class.
100@JsonSubTypes(["test.ObjectMapperTest"])
101@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
102class BadSuperclassModuleInstead:
103 pass
104
105@JsonSubTypes(["test.ObjectMapperTest.AbstractBear"])
106@JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
107class AbstractAnimal(ABC):
108 pass
109
110class AbstractBear(AbstractAnimal):
111 def __init__(self, props:Props):
112 self._props=props
113
114 def __str__(self):
115 return "Bear["+str(self._props)+"]"
116
117 def getprops(self):
118 return self._props
119 def __eq__(self, other):
120 return isinstance(other, self.__class__) and \
121 self._props==other._props
122
123
124# our parser supports non-primitive keys but python dict dont.
125# therefore the following fails even before we can start testing our code...
126# we need to create a hashable dict to get around ths
127class mydict(dict):
128 def __hash__(self, *args, **kwargs):
129 return 1
130
131
132# for testing @JsonValue
133class Snare:
134 def __init__(self, value:int):
135 self._a=value
136 @JsonValue()
137 def getValue(self):
138 return self._a
139 def __eq__(self, other):
140 return isinstance(other, self.__class__) and self._a==other._a
141
142class ContainsSnare:
143 def __init__(self, snare:Snare):
144 self._snare=snare
145 def getSnare(self):
146 return self._snare
147 def __eq__(self, other):
148 return isinstance(other, self.__class__) and self._snare==other._snare
149
150
151@JsonDeserialize(using="test.ObjectMapperTest.MyDeserializer")
152class Custom:
153 def __init__(self, value:Any):
154 self._a=value
155 @JsonValue()
156 def getValue(self):
157 return self._a
158 def __eq__(self, other):
159 return isinstance(other, self.__class__) and \
160 self._a==other._a
161
162
163class MyDeserializer(Deserializer):
164 def deserialize(self, data:object, clas: object) -> Custom:
165 if isinstance(data, float):
166 return Custom(Decimal(str(data)))
167 return Custom(data)
168
169class DefaultOne:
170 '''
171 Has default value
172 '''
173 def __init__(self, value:int=1):
174 self._a=value
175 def getValue(self):
176 return self._a
177 def __eq__(self, other):
178 return isinstance(other, self.__class__) and \
179 self._a==other._a
180
181class DefaultNone(ABC):
182 '''
183 Has default value
184 '''
185 def __init__(self, value:Snare=None):
186 self._a=value
187 def getValue(self)->Snare:
188 return self._a
189 def __eq__(self, other):
190 return isinstance(other, self.__class__) and \
191 self._a==other._a
192
193class WithUnion(ABC):
194 '''
195 Has default value
196 '''
197 def __init__(self, value:Union[Snare,None]=None):
198 self._a=value
199 def getValue(self)->Union[Snare,None]:
200 return self._a
201 def __eq__(self, other):
202 return isinstance(other, self.__class__) and \
203 self._a==other._a
204
205
206
207class ObjectMapperTest(unittest.TestCase):
208 '''
209 Test a lot of back-and-forth cases.
210 FIXME Can we make this a parameterized test?
211 '''
212 pyson=ObjectMapper()
213
214
215 def testPrimitives(self):
216
217 res=self.pyson.parse(3, int)
218 self.assertEqual(3, res)
219
220
221 # this throws correct,
222 self.assertRaises(ValueError, lambda:self.pyson.parse(3, str))
223
224 # this throws correct,
225 self.assertRaises(ValueError, lambda:self.pyson.parse("ja", int))
226
227 #DEMO with nested classes of different types.
228 res=self.pyson.parse('three', str)
229 print(res, type(res))
230
231 self.pyson.parse(3.0, float)
232 self.pyson.parse(3.1, float)
233 self.pyson.parse(3j, complex)
234 self.pyson.parse(range(6), range)
235 self.pyson.parse(True, bool)
236 self.pyson.parse(False, bool)
237 self.pyson.parse(b"Hello", bytes)
238 self.pyson.parse(bytearray(b'\x00\x00\x00\x01'), bytearray)
239
240 jsonlh = "http://localhost/"
241 urilh = URI(jsonlh)
242 self.assertEqual(urilh, self.pyson.parse(jsonlh, URI))
243
244 #WARNING python is cheating us. equals succeeds but the type may be wrong!!
245 # python says URI("hello")=="hello"
246 self.assertEqual(jsonlh, self.pyson.toJson(urilh))
247 self.assertEqual(str, type(self.pyson.toJson(urilh)))
248
249 self.assertEqual(1.1, self.pyson.toJson(Decimal('1.1')))
250 self.assertEqual(Decimal('1.1'), self.pyson.parse(1.1, Decimal))
251
252 def testPrimitiveUUID(self):
253 id=uuid4()
254 self.assertEqual(id, self.pyson.parse(str(id), UUID))
255 self.assertEqual(str(id), self.pyson.toJson(id))
256
257 def testProps(self):
258 propsjson={'age': 10, 'name': 'pietje'}
259 props=Props(10, "pietje")
260 self.assertEqual(propsjson,self.pyson.toJson(props))
261 self.assertEqual(props, self.pyson.parse(propsjson, Props))
262
263 def testParseDeepError(self):
264 propsjson={'age': 10, 'name': 12}
265 try:
266 self.pyson.parse(propsjson, Props)
267 raise AssertionError("parser did not throw")
268 except ValueError as e:
269 # we catch this to assure the exception contains
270 # both top error and details.
271 print("received error "+str(e))
272 self.assertTrue(str(e).find("Error parsing"))
273 self.assertTrue(str(e).find("ValueError"))
274 self.assertTrue(str(e).find("expected"))
275
276
277 def testEmpty(self):
278
279 class EmptyClass:
280 def __init__(self):
281 pass
282 def __eq__(self, other):
283 return isinstance(other, self.__class__)
284
285 obj=EmptyClass()
286 print(self.pyson.toJson(obj))
287 res=self.pyson.parse({}, EmptyClass)
288 self.assertEqual(obj, res)
289
290 def testSubType(self):
291
292 class Cat():
293 def __init__(self, props:Props):
294 self._props=props
295
296 def __str__(self):
297 return "Cat["+str(self._props)+"]"
298
299 def getprops(self):
300 return self._props
301
302 obj=Cat(Props(1,'bruno'))
303 print(self.pyson.toJson(obj))
304
305
306 bson={'props':{'age':1, 'name':'bruno'}}
307 res=self.pyson.parse(bson, Cat)
308 print(res, type(res))
309 self.assertEqual(type(res.getprops()), Props)
310
311 def testDeserializeNoSuchField(self):
312 # Bear has a 'props' field, bot 'b'
313 self.assertRaises(ValueError, lambda:self.pyson.parse({'b':1}, Props))
314 #self.pyson.parse({'b':1}, Props)
315
316 def testInheritance(self):
317 obj=Bear(Props(1,'bruno'))
318 res=self.pyson.toJson(obj)
319 print("result:"+str(res))
320 bson={'Bear': {'props': {'age': 1, 'name': 'bruno'}}}
321 self.assertEqual(bson, res)
322
323 res=self.pyson.parse(bson, Animal)
324 print("Deserialized an Animal! -->"+str(res))
325 self. assertEqual(obj, res)
326
327 def testAbcInheritance(self):
328 obj=AbstractBear(Props(1,'bruno'))
329 res=self.pyson.toJson(obj)
330 print("result:"+str(res))
331 bson={'AbstractBear': {'props': {'age': 1, 'name': 'bruno'}}}
332 self.assertEqual(bson, res)
333
334 res=self.pyson.parse(bson, AbstractAnimal)
335 print("Deserialized an Animal! -->"+str(res))
336 self. assertEqual(obj, res)
337
338
339
340 def testUntypedList(self):
341 class Prim:
342 def __init__(self, a:list):
343 self._a=a
344 def geta(self)->list:
345 return self._a
346 def __eq__(self, other):
347 return isinstance(other, self.__class__) and self._a==other._a
348
349 obj=Prim([1,2])
350 objson = {'a':[1,2]}
351
352 self.assertEqual(objson, self.pyson.toJson(obj))
353 self.assertEqual(obj, self.pyson.parse(objson, Prim))
354
355 def testDateTime(self):
356 objson = 1000120 # 1000.12ms since 1970
357 obj=datetime.fromtimestamp(objson/1000.0);
358 self.assertEqual(objson, self.pyson.toJson(obj))
359 self.assertEqual(obj, self.pyson.parse(objson, datetime))
360
361
362 def testTypedList(self):
363 '''
364 deserializes typed list contained in another object
365 '''
366 class Prim:
367 def __init__(self, a:List[str]):
368 self._a=a
369 def geta(self)->List[str]:
370 return self._a
371 def __eq__(self, other):
372 return isinstance(other, self.__class__) and \
373 self._a==other._a
374
375 obj=Prim(["x","y"])
376 objson = {'a':["x","y"]}
377
378 self.assertEqual(objson, self.pyson.toJson(obj))
379 self.assertEqual(obj, self.pyson.parse(objson, Prim))
380
381 def testTypedListDirect(self):
382 '''
383 deserializes typed list directly
384 '''
385
386 obj=["x","y"]
387 objson = ["x","y"]
388
389 self.assertEqual(objson, self.pyson.toJson(obj))
390 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
391
392 def testMixedDict(self):
393 obj={'a':1, 'b':'blabla'}
394
395 # primitive types, should not be changed
396 self.assertEqual(obj, self.pyson.toJson(obj))
397 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
398
399 def testTypedListOfObjMissingAnnotation(self):
400 class Prim:
401 def __init__(self, a:int):
402 self._a=a
403 def geta(self)->int:
404 return self._a
405 def __eq__(self, other):
406 return isinstance(other, self.__class__) and \
407 self._a==other._a
408 obj=[Prim(1),Prim(3)]
409 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
410
411 # SHOULD WE CHECK THIS?
412 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
413 # object misses annotation, therefore this will try to parse
414 # Prim objects without header here.
415 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
416
417 def testTypedListOfObj(self):
418 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
419 class Prim:
420 def __init__(self, a:int):
421 self._a=a
422 def geta(self)->int:
423 return self._a
424 def __eq__(self, other):
425 return isinstance(other, self.__class__) and \
426 self._a==other._a
427
428 obj=[Prim(1),Prim(3)]
429 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
430 self.assertEqual(objson, self.pyson.toJson(obj))
431 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
432
433 def testTypedSetOfObj(self):
434 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
435 class Prim:
436 def __init__(self, a:int):
437 self._a=a
438 def geta(self)->int:
439 return self._a
440 def __eq__(self, other):
441 return isinstance(other, self.__class__) and \
442 self._a==other._a
443
444 obj=set([SimpleWithHash(1),SimpleWithHash(3)])
445 objson = [{"SimpleWithHash":{'a':1}},{"SimpleWithHash":{'a':3}}]
446 self.assertEqual(objson, self.pyson.toJson(obj))
447 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
448 self.assertEqual(obj, parsedobj)
449
450
451 def testExpectListButGiveDict(self):
452 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
453 class Prim:
454 def __init__(self, a:int):
455 self._a=a
456 def geta(self)->int:
457 return self._a
458 def __eq__(self, other):
459 return isinstance(other, self.__class__) and \
460 self._a==other._a
461
462 objson = { 'a':{"Prim":{'a':1}},'c':{"Prim":{'a':3}}}
463 # we request List but obj is a dict.
464 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
465
466 def testSerializeDict(self):
467 obj={'a':Simple(1),'c':Simple(3)}
468 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
469 self.assertEqual(objson, self.pyson.toJson(obj))
470
471
472 def testTypedDictOfObj(self):
473 obj={'a':Simple(1),'c':Simple(3)}
474 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
475 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
476 print("deserialized obj"+str(objson)+"="+str(obj))
477
478 def testTypedDictSimpleKey(self):
479 key=mydict()
480 key["Simple"]={'a':1}
481 # simple is not hashable
482 objson = { key : 'a' }
483 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
484
485 def testTypedDictSimpleKeyHashable(self):
486 # key is now not primitive!
487 obj={SimpleWithHash(1):'a'}
488
489 # simple is not hashable
490 key=mydict()
491 key["SimpleWithHash"]={'a':1}
492 # simple is not hashable
493 objson = { key : 'a' }
494 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
495
496 def testDeserializeBadSubclass(self):
497 objson= { 'BadSubclass':{ 'a':1}}
498 # FIXME the error message is poor in this case.
499 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
500
501
502 def testModuleInsteadOfClassAsSubclasses(self):
503 objson= { 'BadSubclass':{ 'a':1}}
504 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
505
506
507
508 def testJsonGetter(self):
509 class Getter:
510 def __init__(self, a:int):
511 self._a=a
512 @JsonGetter("a")
513 def getValue(self):
514 return self._a
515 def __eq__(self, other):
516 return isinstance(other, self.__class__) and \
517 self._a==other._a
518
519 getter=Getter(17)
520 objson={'a':17}
521 self.assertEqual(objson, self.pyson.toJson(getter))
522 self.assertEqual(getter, self.pyson.parse(objson, Getter))
523
524 def testGetterIgnoresCase(self):
525 class Getter:
526 def __init__(self, value:int):
527 self._a=value
528 def getValue(self):
529 return self._a
530 def __eq__(self, other):
531 return isinstance(other, self.__class__) and \
532 self._a==other._a
533
534 getter=Getter(17)
535 objson={'value':17}
536 self.assertEqual(objson, self.pyson.toJson(getter))
537 self.assertEqual(getter, self.pyson.parse(objson, Getter))
538
539
540 def testJsonValue(self):
541 getter=Snare(17)
542 objson=17
543 print(self.pyson.toJson(getter))
544 self.assertEqual(objson, self.pyson.toJson(getter))
545 self.assertEqual(getter, self.pyson.parse(objson, Snare))
546
547
548 def testJsonValueAsPart(self):
549 csnare=ContainsSnare(Snare(18))
550 objson={"snare":18}
551 print(self.pyson.toJson(csnare))
552 self.assertEqual(objson, self.pyson.toJson(csnare))
553 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
554
555
556 def testUriJsonValue(self):
557 class UriJson:
558 def __init__(self, value:URI):
559 self._a=value
560 @JsonValue()
561 def getValue(self):
562 return self._a
563 def __eq__(self, other):
564 return isinstance(other, self.__class__) and \
565 self._a==other._a
566
567 objson="http://test/"
568 obj=UriJson(URI(objson))
569 self.assertEqual(objson, self.pyson.toJson(obj))
570 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
571
572
573 def testJsonValueInList(self):
574 snare1=Snare(1)
575 snare2=Snare(2)
576 snareList = [snare1, snare2]
577 self.assertEqual([1, 2],self.pyson.toJson(snareList))
578 self.assertEqual(snareList, self.pyson.parse([1,2], List[Snare]))
579
580
581
582
583 def testJsonDeserializerAnnotation(self):
584 self.assertEqual(Custom(Decimal(1)), self.pyson.parse(1.0, Custom))
585 self.assertEqual(1.0, self.pyson.toJson(Custom(Decimal(1))))
586 self.assertEqual(Custom('bla'), self.pyson.parse('bla', Custom))
587 self.assertEqual('bla', self.pyson.toJson(Custom('bla')))
588
589
590 def testDeserializeMissingValue(self):
591 a=DefaultOne()
592 self.assertEqual(1, a.getValue())
593 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
594
595
596 def testDeserializeDefaultNone(self):
597 a=DefaultNone()
598 self.assertEqual(None, a.getValue())
599 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
600
601 def testDeserializeUnion(self):
602 a=WithUnion(None)
603 self.assertEqual(None, a.getValue())
604 self.assertEqual(a, self.pyson.parse({}, WithUnion))
605
606 def testDeserializeOptional(self):
607 NoneType=type(None)
608 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
609 self.assertEqual(None, defaultone)
610
Note: See TracBrowser for help on using the repository browser.