source: pyson/test/ObjectMapperTest.py@ 306

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

#102 add support for exception (de)serialization

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