source: pyson/test/ObjectMapperTest.py@ 249

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

added more tests related to serializing None, seems all fine

File size: 20.5 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 uri import URI # type:ignore
13
14from pyson.Deserializer import Deserializer
15from pyson.JsonDeserialize import JsonDeserialize
16from pyson.JsonGetter import JsonGetter
17from pyson.JsonSubTypes import JsonSubTypes
18from pyson.JsonTypeInfo import Id, As
19from pyson.JsonTypeInfo import JsonTypeInfo
20from pyson.JsonValue import JsonValue
21from pyson.ObjectMapper import ObjectMapper
22
23
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):
46 self._a=a
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
82
83# A wrongly configured type, you must add
84#@JsonSubTypes(["test.ObjectMapperTest.BadSubclass"])
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
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
123
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
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
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
157
158
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
169
170
171class MyDeserializer(Deserializer):
172 def deserialize(self, data:object, clas: object) -> Custom:
173 if isinstance(data, float):
174 return Custom(Decimal(str(data)))
175 return Custom(data)
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
188
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
200
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
212
213
214
215class ObjectMapperTest(unittest.TestCase):
216 '''
217 Test a lot of back-and-forth cases.
218 FIXME Can we make this a parameterized test?
219 '''
220 pyson=ObjectMapper()
221
222
223 def testPrimitives(self):
224
225 res=self.pyson.parse(3, int)
226 self.assertEqual(3, res)
227
228
229 # this throws correct,
230 self.assertRaises(ValueError, lambda:self.pyson.parse(3, str))
231
232 # this throws correct,
233 self.assertRaises(ValueError, lambda:self.pyson.parse("ja", int))
234
235 #DEMO with nested classes of different types.
236 res=self.pyson.parse('three', str)
237 print(res, type(res))
238
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)
247
248 jsonlh = "http://localhost/"
249 urilh = URI(jsonlh)
250 self.assertEqual(urilh, self.pyson.parse(jsonlh, URI))
251
252 #WARNING python is cheating us. equals succeeds but the type may be wrong!!
253 # python says URI("hello")=="hello"
254 self.assertEqual(jsonlh, self.pyson.toJson(urilh))
255 self.assertEqual(str, type(self.pyson.toJson(urilh)))
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))
262
263 def testDecimal(self):
264 self.assertEqual(1.1, self.pyson.toJson(Decimal('1.1')))
265 self.assertEqual(Decimal('1.1'), self.pyson.parse(1.1, Decimal))
266
267 self.assertEqual(Decimal('1200'), self.pyson.parse(1200, Decimal))
268 v=self.pyson.toJson(Decimal('1200'))
269 self. assertEqual(1200, v)
270
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"
274 # this notation is allowed in json file notation too.
275
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
284 def testPrimitiveUUID(self):
285 id=uuid4()
286 self.assertEqual(id, self.pyson.parse(str(id), UUID))
287 self.assertEqual(str(id), self.pyson.toJson(id))
288
289 def testProps(self):
290 propsjson={'age': 10, 'name': 'pietje'}
291 props=Props(10, "pietje")
292 self.assertEqual(propsjson,self.pyson.toJson(props))
293 self.assertEqual(props, self.pyson.parse(propsjson, Props))
294
295 def testParseDeepError(self):
296 propsjson={'age': 10, 'name': 12}
297 try:
298 self.pyson.parse(propsjson, Props)
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()
318 print(self.pyson.toJson(obj))
319 res=self.pyson.parse({}, EmptyClass)
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'))
335 print(self.pyson.toJson(obj))
336
337
338 bson={'props':{'age':1, 'name':'bruno'}}
339 res=self.pyson.parse(bson, Cat)
340 print(res, type(res))
341 self.assertEqual(type(res.getprops()), Props)
342
343 def testDeserializeNoSuchField(self):
344 # Bear has a 'props' field, bot 'b'
345 self.assertRaises(ValueError, lambda:self.pyson.parse({'b':1}, Props))
346 #self.pyson.parse({'b':1}, Props)
347
348 def testInheritance(self):
349 obj=Bear(Props(1,'bruno'))
350 res=self.pyson.toJson(obj)
351 print("result:"+str(res))
352 bson={'Bear': {'props': {'age': 1, 'name': 'bruno'}}}
353 self.assertEqual(bson, res)
354
355 res=self.pyson.parse(bson, Animal)
356 print("Deserialized an Animal! -->"+str(res))
357 self. assertEqual(obj, res)
358
359 def testAbcInheritance(self):
360 obj=AbstractBear(Props(1,'bruno'))
361 res=self.pyson.toJson(obj)
362 print("result:"+str(res))
363 bson={'AbstractBear': {'props': {'age': 1, 'name': 'bruno'}}}
364 self.assertEqual(bson, res)
365
366 res=self.pyson.parse(bson, AbstractAnimal)
367 print("Deserialized an Animal! -->"+str(res))
368 self. assertEqual(obj, res)
369
370
371
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
378 def __eq__(self, other):
379 return isinstance(other, self.__class__) and self._a==other._a
380
381 obj=Prim([1,2])
382 objson = {'a':[1,2]}
383
384 self.assertEqual(objson, self.pyson.toJson(obj))
385 self.assertEqual(obj, self.pyson.parse(objson, Prim))
386
387 def testDateTime(self):
388 objson = 1000120 # 1000.12ms since 1970
389 obj=datetime.fromtimestamp(objson/1000.0);
390 self.assertEqual(objson, self.pyson.toJson(obj))
391 self.assertEqual(obj, self.pyson.parse(objson, datetime))
392
393
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
410 self.assertEqual(objson, self.pyson.toJson(obj))
411 self.assertEqual(obj, self.pyson.parse(objson, Prim))
412
413 def testTypedListDirect(self):
414 '''
415 deserializes typed list directly
416 '''
417
418 obj=["x","y"]
419 objson = ["x","y"]
420
421 self.assertEqual(objson, self.pyson.toJson(obj))
422 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
423
424 def testMixedDict(self):
425 obj={'a':1, 'b':'blabla'}
426
427 # primitive types, should not be changed
428 self.assertEqual(obj, self.pyson.toJson(obj))
429 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
430
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}}]
442
443 # SHOULD WE CHECK THIS?
444 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
445 # object misses annotation, therefore this will try to parse
446 # Prim objects without header here.
447 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
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}}]
462 self.assertEqual(objson, self.pyson.toJson(obj))
463 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
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}}]
478 self.assertEqual(objson, self.pyson.toJson(obj))
479 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
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.
496 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
497
498 def testSerializeDict(self):
499 obj={'a':Simple(1),'c':Simple(3)}
500 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
501 self.assertEqual(objson, self.pyson.toJson(obj))
502
503
504 def testTypedDictOfObj(self):
505 obj={'a':Simple(1),'c':Simple(3)}
506 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
507 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
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' }
515 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
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' }
526 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
527
528 def testDeserializeBadSubclass(self):
529 objson= { 'BadSubclass':{ 'a':1}}
530 # FIXME the error message is poor in this case.
531 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
532
533
534 def testModuleInsteadOfClassAsSubclasses(self):
535 objson= { 'BadSubclass':{ 'a':1}}
536 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
537
538
539
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}
553 self.assertEqual(objson, self.pyson.toJson(getter))
554 self.assertEqual(getter, self.pyson.parse(objson, Getter))
555
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}
568 self.assertEqual(objson, self.pyson.toJson(getter))
569 self.assertEqual(getter, self.pyson.parse(objson, Getter))
570
571
572 def testJsonValue(self):
573 getter=Snare(17)
574 objson=17
575 print(self.pyson.toJson(getter))
576 self.assertEqual(objson, self.pyson.toJson(getter))
577 val:Snare = self.pyson.parse(objson, Snare)
578 self.assertEqual(getter, val)
579
580
581 def testJsonValueAsPart(self):
582 csnare=ContainsSnare(Snare(18))
583 objson={"snare":18}
584 print(self.pyson.toJson(csnare))
585 self.assertEqual(objson, self.pyson.toJson(csnare))
586 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
587
588
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))
602 self.assertEqual(objson, self.pyson.toJson(obj))
603 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
604
605
606 def testJsonValueInList(self):
607 snare1=Snare(1)
608 snare2=Snare(2)
609 snareList = [snare1, snare2]
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')))
621
622
623 def testDeserializeMissingValue(self):
624 a=DefaultOne()
625 self.assertEqual(1, a.getValue())
626 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
627
628
629 def testDeserializeDefaultNone(self):
630 a=DefaultNone()
631 self.assertEqual(None, a.getValue())
632 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
633
634 def testDeserializeUnion(self):
635 a=WithUnion(None)
636 self.assertEqual(None, a.getValue())
637 self.assertEqual(a, self.pyson.parse({}, WithUnion))
638
639 def testDeserializeOptional(self):
640 NoneType=type(None)
641 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
642 self.assertEqual(None, defaultone)
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.