source: pyson/test/ObjectMapperTest.py@ 239

Last change on this file since 239 was 239, checked in by wouter, 4 years ago

added more tests related to serializing None, seems all fine

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