source: pyson/test/ObjectMapperTest.py@ 1227

Last change on this file since 1227 was 1144, checked in by wouter, 2 months ago

#366 added test showing the issue

File size: 22.6 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
164class MyDeserializer(Deserializer):
165 def deserialize(self, data:object, clas: object) -> "Custom":
166 if isinstance(data, float):
167 return Custom(Decimal(str(data)))
168 return Custom(data)
169
170
171@JsonDeserialize(MyDeserializer)
172class Custom:
173 def __init__(self, value:Any):
174 self._a=value
175 @JsonValue()
176 def getValue(self):
177 return self._a
178 def __eq__(self, other):
179 return isinstance(other, self.__class__) and \
180 self._a==other._a
181
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
225class FrozenTest(ABC):
226 def __init__(self, value:Set[str]):
227 self._value=frozenset(value)
228 def getValue(self) -> Set[str]:
229 return self._value
230
231
232class StringContainer(ABC):
233 def __init__(self, val:str):
234 self._val = val
235 @JsonValue()
236 def getValue(self) -> str:
237 return self._val
238
239
240
241
242
243
244
245
246
247
248
249
250
251class ObjectMapperTest(unittest.TestCase):
252 '''
253 Test a lot of back-and-forth cases.
254 FIXME Can we make this a parameterized test?
255 '''
256 pyson=ObjectMapper()
257
258
259 def testPrimitives(self):
260
261 res=self.pyson.parse(3, int)
262 self.assertEqual(3, res)
263
264
265 # this throws correct,
266 self.assertRaises(ValueError, lambda:self.pyson.parse(3, str))
267
268 # this throws correct,
269 self.assertRaises(ValueError, lambda:self.pyson.parse("ja", int))
270
271 #DEMO with nested classes of different types.
272 res=self.pyson.parse('three', str)
273 print(res, type(res))
274
275 self.pyson.parse(3.0, float)
276 self.pyson.parse(3.1, float)
277 self.pyson.parse(3j, complex)
278 self.pyson.parse(range(6), range)
279 self.pyson.parse(True, bool)
280 self.pyson.parse(False, bool)
281 self.pyson.parse(b"Hello", bytes)
282 self.pyson.parse(bytearray(b'\x00\x00\x00\x01'), bytearray)
283
284 jsonlh = "http://localhost/"
285 urilh = URI(jsonlh)
286 self.assertEqual(urilh, self.pyson.parse(jsonlh, URI))
287
288 #WARNING python is cheating us. equals succeeds but the type may be wrong!!
289 # python says URI("hello")=="hello"
290 self.assertEqual(jsonlh, self.pyson.toJson(urilh))
291 self.assertEqual(str, type(self.pyson.toJson(urilh)))
292
293 def testNone(self):
294 self.assertEqual(None, self.pyson.toJson(None))
295 self.assertEqual('null', json.dumps(None))
296 self.assertEqual(None, json.loads('null'))
297 self.assertEqual(None, self.pyson.parse(None, Any))
298
299 def testDecimal(self):
300 self.assertEqual(1.1, self.pyson.toJson(Decimal('1.1')))
301 self.assertEqual(Decimal('1.1'), self.pyson.parse(1.1, Decimal))
302
303 self.assertEqual(Decimal('1200'), self.pyson.parse(1200, Decimal))
304 v=self.pyson.toJson(Decimal('1200'))
305 self. assertEqual(1200, v)
306
307 # 3/40000 triggers scientific notation in python
308 v=self.pyson.toJson(Decimal(3/40000.))
309 print(str(v)) # this will print "7.5e-05"
310 # this notation is allowed in json file notation too.
311
312 def testDecimalSerializationType(self):
313 v=self.pyson.toJson(Decimal('1200'))
314 self.assertEqual(int, type(v))
315
316 v=self.pyson.toJson(Decimal('1200.1'))
317 self.assertEqual(float, type(v))
318
319
320 def testPrimitiveUUID(self):
321 id=uuid4()
322 self.assertEqual(id, self.pyson.parse(str(id), UUID))
323 self.assertEqual(str(id), self.pyson.toJson(id))
324
325 def testProps(self):
326 propsjson={'age': 10, 'name': 'pietje'}
327 props=Props(10, "pietje")
328 self.assertEqual(propsjson,self.pyson.toJson(props))
329 self.assertEqual(props, self.pyson.parse(propsjson, Props))
330
331 def testParseDeepError(self):
332 propsjson={'age': 10, 'name': 12}
333 try:
334 self.pyson.parse(propsjson, Props)
335 raise AssertionError("parser did not throw")
336 except ValueError as e:
337 # we catch this to assure the exception contains
338 # both top error and details.
339 print("received error "+str(e))
340 self.assertTrue(str(e).find("Error parsing"))
341 self.assertTrue(str(e).find("ValueError"))
342 self.assertTrue(str(e).find("expected"))
343
344
345 def testEmpty(self):
346
347 class EmptyClass:
348 def __init__(self):
349 pass
350 def __eq__(self, other):
351 return isinstance(other, self.__class__)
352
353 obj=EmptyClass()
354 print(self.pyson.toJson(obj))
355 res=self.pyson.parse({}, EmptyClass)
356 self.assertEqual(obj, res)
357
358 def testSubType(self):
359
360 class Cat():
361 def __init__(self, props:Props):
362 self._props=props
363
364 def __str__(self):
365 return "Cat["+str(self._props)+"]"
366
367 def getprops(self):
368 return self._props
369
370 obj=Cat(Props(1,'bruno'))
371 print(self.pyson.toJson(obj))
372
373
374 bson={'props':{'age':1, 'name':'bruno'}}
375 res=self.pyson.parse(bson, Cat)
376 print(res, type(res))
377 self.assertEqual(type(res.getprops()), Props)
378
379 def testDeserializeNoSuchField(self):
380 # Bear has a 'props' field, bot 'b'
381 self.assertRaises(ValueError, lambda:self.pyson.parse({'b':1}, Props))
382 #self.pyson.parse({'b':1}, Props)
383
384 def testInheritance(self):
385 obj=Bear(Props(1,'bruno'))
386 res=self.pyson.toJson(obj)
387 print("result:"+str(res))
388 bson={'Bear': {'props': {'age': 1, 'name': 'bruno'}}}
389 self.assertEqual(bson, res)
390
391 res=self.pyson.parse(bson, Animal)
392 print("Deserialized an Animal! -->"+str(res))
393 self. assertEqual(obj, res)
394
395 def testAbcInheritance(self):
396 obj=AbstractBear(Props(1,'bruno'))
397 res=self.pyson.toJson(obj)
398 print("result:"+str(res))
399 bson={'AbstractBear': {'props': {'age': 1, 'name': 'bruno'}}}
400 self.assertEqual(bson, res)
401
402 res=self.pyson.parse(bson, AbstractAnimal)
403 print("Deserialized an Animal! -->"+str(res))
404 self. assertEqual(obj, res)
405
406
407
408 def testUntypedList(self):
409 class Prim:
410 def __init__(self, a:list):
411 self._a=a
412 def geta(self)->list:
413 return self._a
414 def __eq__(self, other):
415 return isinstance(other, self.__class__) and self._a==other._a
416
417 obj=Prim([1,2])
418 objson = {'a':[1,2]}
419
420 self.assertEqual(objson, self.pyson.toJson(obj))
421 self.assertEqual(obj, self.pyson.parse(objson, Prim))
422
423 def testDateTime(self):
424 objson = 1000120 # 1000.12ms since 1970
425 obj=datetime.fromtimestamp(objson/1000.0);
426 self.assertEqual(objson, self.pyson.toJson(obj))
427 self.assertEqual(obj, self.pyson.parse(objson, datetime))
428
429 def testDateTime2(self):
430 for t in range(999, 1010):
431 obj=datetime.fromtimestamp(t/1000.)
432 self.assertEqual(t, self.pyson.toJson(obj))
433
434
435 def testTypedList(self):
436 '''
437 deserializes typed list contained in another object
438 '''
439 class Prim:
440 def __init__(self, a:List[str]):
441 self._a=a
442 def geta(self)->List[str]:
443 return self._a
444 def __eq__(self, other):
445 return isinstance(other, self.__class__) and \
446 self._a==other._a
447
448 obj=Prim(["x","y"])
449 objson = {'a':["x","y"]}
450
451 self.assertEqual(objson, self.pyson.toJson(obj))
452 self.assertEqual(obj, self.pyson.parse(objson, Prim))
453
454 def testTypedListDirect(self):
455 '''
456 deserializes typed list directly
457 '''
458
459 obj=["x","y"]
460 objson = ["x","y"]
461
462 self.assertEqual(objson, self.pyson.toJson(obj))
463 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
464
465 def testMixedDict(self):
466 obj={'a':1, 'b':'blabla'}
467
468 # primitive types, should not be changed
469 self.assertEqual(obj, self.pyson.toJson(obj))
470 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
471
472 def testTypedListOfObjMissingAnnotation(self):
473 class Prim:
474 def __init__(self, a:int):
475 self._a=a
476 def geta(self)->int:
477 return self._a
478 def __eq__(self, other):
479 return isinstance(other, self.__class__) and \
480 self._a==other._a
481 obj=[Prim(1),Prim(3)]
482 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
483
484 # SHOULD WE CHECK THIS?
485 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
486 # object misses annotation, therefore this will try to parse
487 # Prim objects without header here.
488 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
489
490 def testTypedListOfObj(self):
491 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
492 class Prim:
493 def __init__(self, a:int):
494 self._a=a
495 def geta(self)->int:
496 return self._a
497 def __eq__(self, other):
498 return isinstance(other, self.__class__) and \
499 self._a==other._a
500
501 obj=[Prim(1),Prim(3)]
502 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
503 self.assertEqual(objson, self.pyson.toJson(obj))
504 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
505
506 def testTypedSetOfObj(self):
507 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
508 class Prim:
509 def __init__(self, a:int):
510 self._a=a
511 def geta(self)->int:
512 return self._a
513 def __eq__(self, other):
514 return isinstance(other, self.__class__) and \
515 self._a==other._a
516
517 obj=set([SimpleWithHash(1),SimpleWithHash(3)])
518 objson = [{"SimpleWithHash":{'a':1}},{"SimpleWithHash":{'a':3}}]
519 self.assertEqual(objson, self.pyson.toJson(obj))
520 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
521 self.assertEqual(obj, parsedobj)
522
523
524 def testExpectListButGiveDict(self):
525 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
526 class Prim:
527 def __init__(self, a:int):
528 self._a=a
529 def geta(self)->int:
530 return self._a
531 def __eq__(self, other):
532 return isinstance(other, self.__class__) and \
533 self._a==other._a
534
535 objson = { 'a':{"Prim":{'a':1}},'c':{"Prim":{'a':3}}}
536 # we request List but obj is a dict.
537 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
538
539 def testSerializeDict(self):
540 obj={'a':Simple(1),'c':Simple(3)}
541 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
542 self.assertEqual(objson, self.pyson.toJson(obj))
543
544
545 def testTypedDictOfObj(self):
546 obj={'a':Simple(1),'c':Simple(3)}
547 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
548 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
549 print("deserialized obj"+str(objson)+"="+str(obj))
550
551 def testTypedDictSimpleKey(self):
552 key=mydict()
553 key["Simple"]={'a':1}
554 # simple is not hashable
555 objson = { key : 'a' }
556 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
557
558 def testTypedDictSimpleKeyHashable(self):
559 # key is now an object. Pyson has special provision to handle these.
560 # the object is serialized to a string
561 obj={SimpleWithHash(1):'a'}
562 objson={"{\"SimpleWithHash\": {\"a\": 1}}": "a"}
563
564 print("to obj:"+json.dumps(self.pyson.toJson(obj)))
565 self.assertEqual(objson,self.pyson.toJson(obj))
566
567 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
568
569 def testDeserializeBadSubclass(self):
570 objson= { 'BadSubclass':{ 'a':1}}
571 # FIXME the error message is poor in this case.
572 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
573
574
575 def testModuleInsteadOfClassAsSubclasses(self):
576 objson= { 'BadSubclass':{ 'a':1}}
577 self.assertRaises(TypeError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
578
579
580
581 def testJsonGetter(self):
582 class Getter:
583 def __init__(self, a:int):
584 self._a=a
585 @JsonGetter("a")
586 def getValue(self):
587 return self._a
588 def __eq__(self, other):
589 return isinstance(other, self.__class__) and \
590 self._a==other._a
591
592 getter=Getter(17)
593 objson={'a':17}
594 self.assertEqual(objson, self.pyson.toJson(getter))
595 self.assertEqual(getter, self.pyson.parse(objson, Getter))
596
597 def testGetterIgnoresCase(self):
598 class Getter:
599 def __init__(self, value:int):
600 self._a=value
601 def getValue(self):
602 return self._a
603 def __eq__(self, other):
604 return isinstance(other, self.__class__) and \
605 self._a==other._a
606
607 getter=Getter(17)
608 objson={'value':17}
609 self.assertEqual(objson, self.pyson.toJson(getter))
610 self.assertEqual(getter, self.pyson.parse(objson, Getter))
611
612
613 def testJsonValue(self):
614 getter=Snare(17)
615 objson=17
616 print(self.pyson.toJson(getter))
617 self.assertEqual(objson, self.pyson.toJson(getter))
618 val:Snare = self.pyson.parse(objson, Snare)
619 self.assertEqual(getter, val)
620
621
622 def testJsonValueAsPart(self):
623 csnare=ContainsSnare(Snare(18))
624 objson={"snare":18}
625 print(self.pyson.toJson(csnare))
626 self.assertEqual(objson, self.pyson.toJson(csnare))
627 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
628
629
630 def testUriJsonValue(self):
631 class UriJson:
632 def __init__(self, value:URI):
633 self._a=value
634 @JsonValue()
635 def getValue(self):
636 return self._a
637 def __eq__(self, other):
638 return isinstance(other, self.__class__) and \
639 self._a==other._a
640
641 objson="http://test/"
642 obj=UriJson(URI(objson))
643 self.assertEqual(objson, self.pyson.toJson(obj))
644 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
645
646
647 def testJsonValueInList(self):
648 snare1=Snare(1)
649 snare2=Snare(2)
650 snareList = [snare1, snare2]
651 self.assertEqual([1, 2],self.pyson.toJson(snareList))
652 self.assertEqual(snareList, self.pyson.parse([1,2], List[Snare]))
653
654
655
656
657 def testJsonDeserializerAnnotation(self):
658 self.assertEqual(Custom(Decimal(1)), self.pyson.parse(1.0, Custom))
659 self.assertEqual(1.0, self.pyson.toJson(Custom(Decimal(1))))
660 self.assertEqual(Custom('bla'), self.pyson.parse('bla', Custom))
661 self.assertEqual('bla', self.pyson.toJson(Custom('bla')))
662
663
664 def testDeserializeMissingValue(self):
665 a=DefaultOne()
666 self.assertEqual(1, a.getValue())
667 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
668
669
670 def testDeserializeDefaultNone(self):
671 a=DefaultNone()
672 self.assertEqual(None, a.getValue())
673 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
674
675 def testDeserializeUnion(self):
676 a=WithUnion(None)
677 self.assertEqual(None, a.getValue())
678 self.assertEqual(a, self.pyson.parse({}, WithUnion))
679
680 def testDeserializeOptional(self):
681 NoneType=type(None)
682 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
683 self.assertEqual(None, defaultone)
684 self.assertEqual(OptionalVal(None), self.pyson.parse({"value":None}, OptionalVal))
685
686 def testSerializeOptional(self):
687 val=OptionalVal(None)
688 self.assertEqual({"value":None}, self.pyson.toJson(val))
689
690 def testSerializeExc(self):
691 self.assertEqual(exceptionjson, self.pyson.toJson(exception))
692 self.assertEqual(exceptionjson, self.pyson.toJson(exception))
693
694 def testDeserializeExc(self):
695 self.assertTrue(errorsEqual(ValueError("err"),
696 self.pyson.parse({'message':'err'}, ValueError)))
697
698 self.assertTrue(errorsEqual(Exception("err"),
699 self.pyson.parse({'message':'err'}, Exception)))
700 self.assertEquals(None, self.pyson.parse(None, Optional[Exception]))
701
702 e=self.pyson.parse(exceptionjson, Optional[ValueError])
703 print(e)
704 expected=ValueError("some error")
705 expected.__cause__=Exception("div zero")
706
707 errorsEqual(expected, e)
708
709 def testFrozenSerializer(self):
710 data = FrozenTest(set([1,2,3]))
711 self.assertEquals({'value': [1, 2, 3]}, self.pyson.toJson(data))
712
713 def testDictWithJsonValueAsKey(self):
714 d:Dict[StringContainer, str]={StringContainer("test"):"value"}
715 self.assertEquals({"test":"value"}, self.pyson.toJson(d))
716
717
Note: See TracBrowser for help on using the repository browser.