source: pyson/test/ObjectMapperTest.py@ 1140

Last change on this file since 1140 was 1140, checked in by wouter, 5 weeks ago

#365 fix frozenset handling in pyson

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