source: pyson/test/ObjectMapperTest.py@ 311

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

#104 added test showing round error

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