source: pyson/test/ObjectMapperTest.py@ 570

Last change on this file since 570 was 570, checked in by wouter, 16 months ago

#191 pyson now automatically recognises dict and converts string keys to objects

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