source: pyson/test/ObjectMapperTest.py@ 958

Last change on this file since 958 was 571, checked in by wouter, 17 months ago

#192 pyson @JsonDeserialize now takes class instead of str

File size: 22.0 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
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 def testDateTime2(self):
418 for t in range(999, 1010):
419 obj=datetime.fromtimestamp(t/1000.)
420 self.assertEqual(t, self.pyson.toJson(obj))
421
422
423 def testTypedList(self):
424 '''
425 deserializes typed list contained in another object
426 '''
427 class Prim:
428 def __init__(self, a:List[str]):
429 self._a=a
430 def geta(self)->List[str]:
431 return self._a
432 def __eq__(self, other):
433 return isinstance(other, self.__class__) and \
434 self._a==other._a
435
436 obj=Prim(["x","y"])
437 objson = {'a':["x","y"]}
438
439 self.assertEqual(objson, self.pyson.toJson(obj))
440 self.assertEqual(obj, self.pyson.parse(objson, Prim))
441
442 def testTypedListDirect(self):
443 '''
444 deserializes typed list directly
445 '''
446
447 obj=["x","y"]
448 objson = ["x","y"]
449
450 self.assertEqual(objson, self.pyson.toJson(obj))
451 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
452
453 def testMixedDict(self):
454 obj={'a':1, 'b':'blabla'}
455
456 # primitive types, should not be changed
457 self.assertEqual(obj, self.pyson.toJson(obj))
458 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
459
460 def testTypedListOfObjMissingAnnotation(self):
461 class Prim:
462 def __init__(self, a:int):
463 self._a=a
464 def geta(self)->int:
465 return self._a
466 def __eq__(self, other):
467 return isinstance(other, self.__class__) and \
468 self._a==other._a
469 obj=[Prim(1),Prim(3)]
470 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
471
472 # SHOULD WE CHECK THIS?
473 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
474 # object misses annotation, therefore this will try to parse
475 # Prim objects without header here.
476 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
477
478 def testTypedListOfObj(self):
479 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
480 class Prim:
481 def __init__(self, a:int):
482 self._a=a
483 def geta(self)->int:
484 return self._a
485 def __eq__(self, other):
486 return isinstance(other, self.__class__) and \
487 self._a==other._a
488
489 obj=[Prim(1),Prim(3)]
490 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
491 self.assertEqual(objson, self.pyson.toJson(obj))
492 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
493
494 def testTypedSetOfObj(self):
495 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
496 class Prim:
497 def __init__(self, a:int):
498 self._a=a
499 def geta(self)->int:
500 return self._a
501 def __eq__(self, other):
502 return isinstance(other, self.__class__) and \
503 self._a==other._a
504
505 obj=set([SimpleWithHash(1),SimpleWithHash(3)])
506 objson = [{"SimpleWithHash":{'a':1}},{"SimpleWithHash":{'a':3}}]
507 self.assertEqual(objson, self.pyson.toJson(obj))
508 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
509 self.assertEqual(obj, parsedobj)
510
511
512 def testExpectListButGiveDict(self):
513 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
514 class Prim:
515 def __init__(self, a:int):
516 self._a=a
517 def geta(self)->int:
518 return self._a
519 def __eq__(self, other):
520 return isinstance(other, self.__class__) and \
521 self._a==other._a
522
523 objson = { 'a':{"Prim":{'a':1}},'c':{"Prim":{'a':3}}}
524 # we request List but obj is a dict.
525 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
526
527 def testSerializeDict(self):
528 obj={'a':Simple(1),'c':Simple(3)}
529 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
530 self.assertEqual(objson, self.pyson.toJson(obj))
531
532
533 def testTypedDictOfObj(self):
534 obj={'a':Simple(1),'c':Simple(3)}
535 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
536 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
537 print("deserialized obj"+str(objson)+"="+str(obj))
538
539 def testTypedDictSimpleKey(self):
540 key=mydict()
541 key["Simple"]={'a':1}
542 # simple is not hashable
543 objson = { key : 'a' }
544 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
545
546 def testTypedDictSimpleKeyHashable(self):
547 # key is now an object. Pyson has special provision to handle these.
548 # the object is serialized to a string
549 obj={SimpleWithHash(1):'a'}
550 objson={"{\"SimpleWithHash\": {\"a\": 1}}": "a"}
551
552 print("to obj:"+json.dumps(self.pyson.toJson(obj)))
553 self.assertEqual(objson,self.pyson.toJson(obj))
554
555 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
556
557 def testDeserializeBadSubclass(self):
558 objson= { 'BadSubclass':{ 'a':1}}
559 # FIXME the error message is poor in this case.
560 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
561
562
563 def testModuleInsteadOfClassAsSubclasses(self):
564 objson= { 'BadSubclass':{ 'a':1}}
565 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
566
567
568
569 def testJsonGetter(self):
570 class Getter:
571 def __init__(self, a:int):
572 self._a=a
573 @JsonGetter("a")
574 def getValue(self):
575 return self._a
576 def __eq__(self, other):
577 return isinstance(other, self.__class__) and \
578 self._a==other._a
579
580 getter=Getter(17)
581 objson={'a':17}
582 self.assertEqual(objson, self.pyson.toJson(getter))
583 self.assertEqual(getter, self.pyson.parse(objson, Getter))
584
585 def testGetterIgnoresCase(self):
586 class Getter:
587 def __init__(self, value:int):
588 self._a=value
589 def getValue(self):
590 return self._a
591 def __eq__(self, other):
592 return isinstance(other, self.__class__) and \
593 self._a==other._a
594
595 getter=Getter(17)
596 objson={'value':17}
597 self.assertEqual(objson, self.pyson.toJson(getter))
598 self.assertEqual(getter, self.pyson.parse(objson, Getter))
599
600
601 def testJsonValue(self):
602 getter=Snare(17)
603 objson=17
604 print(self.pyson.toJson(getter))
605 self.assertEqual(objson, self.pyson.toJson(getter))
606 val:Snare = self.pyson.parse(objson, Snare)
607 self.assertEqual(getter, val)
608
609
610 def testJsonValueAsPart(self):
611 csnare=ContainsSnare(Snare(18))
612 objson={"snare":18}
613 print(self.pyson.toJson(csnare))
614 self.assertEqual(objson, self.pyson.toJson(csnare))
615 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
616
617
618 def testUriJsonValue(self):
619 class UriJson:
620 def __init__(self, value:URI):
621 self._a=value
622 @JsonValue()
623 def getValue(self):
624 return self._a
625 def __eq__(self, other):
626 return isinstance(other, self.__class__) and \
627 self._a==other._a
628
629 objson="http://test/"
630 obj=UriJson(URI(objson))
631 self.assertEqual(objson, self.pyson.toJson(obj))
632 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
633
634
635 def testJsonValueInList(self):
636 snare1=Snare(1)
637 snare2=Snare(2)
638 snareList = [snare1, snare2]
639 self.assertEqual([1, 2],self.pyson.toJson(snareList))
640 self.assertEqual(snareList, self.pyson.parse([1,2], List[Snare]))
641
642
643
644
645 def testJsonDeserializerAnnotation(self):
646 self.assertEqual(Custom(Decimal(1)), self.pyson.parse(1.0, Custom))
647 self.assertEqual(1.0, self.pyson.toJson(Custom(Decimal(1))))
648 self.assertEqual(Custom('bla'), self.pyson.parse('bla', Custom))
649 self.assertEqual('bla', self.pyson.toJson(Custom('bla')))
650
651
652 def testDeserializeMissingValue(self):
653 a=DefaultOne()
654 self.assertEqual(1, a.getValue())
655 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
656
657
658 def testDeserializeDefaultNone(self):
659 a=DefaultNone()
660 self.assertEqual(None, a.getValue())
661 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
662
663 def testDeserializeUnion(self):
664 a=WithUnion(None)
665 self.assertEqual(None, a.getValue())
666 self.assertEqual(a, self.pyson.parse({}, WithUnion))
667
668 def testDeserializeOptional(self):
669 NoneType=type(None)
670 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
671 self.assertEqual(None, defaultone)
672 self.assertEqual(OptionalVal(None), self.pyson.parse({"value":None}, OptionalVal))
673
674 def testSerializeOptional(self):
675 val=OptionalVal(None)
676 self.assertEqual({"value":None}, self.pyson.toJson(val))
677
678 def testSerializeExc(self):
679 self.assertEqual(exceptionjson, self.pyson.toJson(exception))
680 self.assertEqual(exceptionjson, self.pyson.toJson(exception))
681
682 def testDeserializeExc(self):
683 self.assertTrue(errorsEqual(ValueError("err"),
684 self.pyson.parse({'message':'err'}, ValueError)))
685
686 self.assertTrue(errorsEqual(Exception("err"),
687 self.pyson.parse({'message':'err'}, Exception)))
688 self.assertEquals(None, self.pyson.parse(None, Optional[Exception]))
689
690 e=self.pyson.parse(exceptionjson, Optional[ValueError])
691 print(e)
692 expected=ValueError("some error")
693 expected.__cause__=Exception("div zero")
694
695 errorsEqual(expected, e)
696
Note: See TracBrowser for help on using the repository browser.