source: pyson/test/ObjectMapperTest.py@ 225

Last change on this file since 225 was 213, checked in by wouter, 3 years ago
File size: 19.8 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
9import unittest
10from uuid import uuid4, UUID
11
12from uri import URI
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
150
151@JsonDeserialize(using="test.ObjectMapperTest.MyDeserializer")
152class Custom:
153 def __init__(self, value:Any):
154 self._a=value
155 @JsonValue()
156 def getValue(self):
157 return self._a
158 def __eq__(self, other):
159 return isinstance(other, self.__class__) and \
160 self._a==other._a
161
162
163class MyDeserializer(Deserializer):
164 def deserialize(self, data:object, clas: object) -> Custom:
165 if isinstance(data, float):
166 return Custom(Decimal(str(data)))
167 return Custom(data)
168
169class DefaultOne:
170 '''
171 Has default value
172 '''
173 def __init__(self, value:int=1):
174 self._a=value
175 def getValue(self):
176 return self._a
177 def __eq__(self, other):
178 return isinstance(other, self.__class__) and \
179 self._a==other._a
180
181class DefaultNone(ABC):
182 '''
183 Has default value
184 '''
185 def __init__(self, value:Snare=None):
186 self._a=value
187 def getValue(self)->Snare:
188 return self._a
189 def __eq__(self, other):
190 return isinstance(other, self.__class__) and \
191 self._a==other._a
192
193class WithUnion(ABC):
194 '''
195 Has default value
196 '''
197 def __init__(self, value:Union[Snare,None]=None):
198 self._a=value
199 def getValue(self)->Union[Snare,None]:
200 return self._a
201 def __eq__(self, other):
202 return isinstance(other, self.__class__) and \
203 self._a==other._a
204
205
206
207class ObjectMapperTest(unittest.TestCase):
208 '''
209 Test a lot of back-and-forth cases.
210 FIXME Can we make this a parameterized test?
211 '''
212 pyson=ObjectMapper()
213
214
215 def testPrimitives(self):
216
217 res=self.pyson.parse(3, int)
218 self.assertEqual(3, res)
219
220
221 # this throws correct,
222 self.assertRaises(ValueError, lambda:self.pyson.parse(3, str))
223
224 # this throws correct,
225 self.assertRaises(ValueError, lambda:self.pyson.parse("ja", int))
226
227 #DEMO with nested classes of different types.
228 res=self.pyson.parse('three', str)
229 print(res, type(res))
230
231 self.pyson.parse(3.0, float)
232 self.pyson.parse(3.1, float)
233 self.pyson.parse(3j, complex)
234 self.pyson.parse(range(6), range)
235 self.pyson.parse(True, bool)
236 self.pyson.parse(False, bool)
237 self.pyson.parse(b"Hello", bytes)
238 self.pyson.parse(bytearray(b'\x00\x00\x00\x01'), bytearray)
239
240 jsonlh = "http://localhost/"
241 urilh = URI(jsonlh)
242 self.assertEqual(urilh, self.pyson.parse(jsonlh, URI))
243
244 #WARNING python is cheating us. equals succeeds but the type may be wrong!!
245 # python says URI("hello")=="hello"
246 self.assertEqual(jsonlh, self.pyson.toJson(urilh))
247 self.assertEqual(str, type(self.pyson.toJson(urilh)))
248
249 def testDecimal(self):
250 self.assertEqual(1.1, self.pyson.toJson(Decimal('1.1')))
251 self.assertEqual(Decimal('1.1'), self.pyson.parse(1.1, Decimal))
252
253 self.assertEqual(Decimal('1200'), self.pyson.parse(1200, Decimal))
254 v=self.pyson.toJson(Decimal('1200'))
255 self. assertEqual(1200, v)
256
257 # 3/40000 triggers scientific notation in python
258 v=self.pyson.toJson(Decimal(3/40000.))
259 print(str(v)) # this will print "7.5e-05"
260 # this notation is allowed in json file notation too.
261
262 def testDecimalSerializationType(self):
263 v=self.pyson.toJson(Decimal('1200'))
264 self.assertEquals(int, type(v))
265
266 v=self.pyson.toJson(Decimal('1200.1'))
267 self.assertEquals(float, type(v))
268
269
270 def testPrimitiveUUID(self):
271 id=uuid4()
272 self.assertEqual(id, self.pyson.parse(str(id), UUID))
273 self.assertEqual(str(id), self.pyson.toJson(id))
274
275 def testProps(self):
276 propsjson={'age': 10, 'name': 'pietje'}
277 props=Props(10, "pietje")
278 self.assertEqual(propsjson,self.pyson.toJson(props))
279 self.assertEqual(props, self.pyson.parse(propsjson, Props))
280
281 def testParseDeepError(self):
282 propsjson={'age': 10, 'name': 12}
283 try:
284 self.pyson.parse(propsjson, Props)
285 raise AssertionError("parser did not throw")
286 except ValueError as e:
287 # we catch this to assure the exception contains
288 # both top error and details.
289 print("received error "+str(e))
290 self.assertTrue(str(e).find("Error parsing"))
291 self.assertTrue(str(e).find("ValueError"))
292 self.assertTrue(str(e).find("expected"))
293
294
295 def testEmpty(self):
296
297 class EmptyClass:
298 def __init__(self):
299 pass
300 def __eq__(self, other):
301 return isinstance(other, self.__class__)
302
303 obj=EmptyClass()
304 print(self.pyson.toJson(obj))
305 res=self.pyson.parse({}, EmptyClass)
306 self.assertEqual(obj, res)
307
308 def testSubType(self):
309
310 class Cat():
311 def __init__(self, props:Props):
312 self._props=props
313
314 def __str__(self):
315 return "Cat["+str(self._props)+"]"
316
317 def getprops(self):
318 return self._props
319
320 obj=Cat(Props(1,'bruno'))
321 print(self.pyson.toJson(obj))
322
323
324 bson={'props':{'age':1, 'name':'bruno'}}
325 res=self.pyson.parse(bson, Cat)
326 print(res, type(res))
327 self.assertEqual(type(res.getprops()), Props)
328
329 def testDeserializeNoSuchField(self):
330 # Bear has a 'props' field, bot 'b'
331 self.assertRaises(ValueError, lambda:self.pyson.parse({'b':1}, Props))
332 #self.pyson.parse({'b':1}, Props)
333
334 def testInheritance(self):
335 obj=Bear(Props(1,'bruno'))
336 res=self.pyson.toJson(obj)
337 print("result:"+str(res))
338 bson={'Bear': {'props': {'age': 1, 'name': 'bruno'}}}
339 self.assertEqual(bson, res)
340
341 res=self.pyson.parse(bson, Animal)
342 print("Deserialized an Animal! -->"+str(res))
343 self. assertEqual(obj, res)
344
345 def testAbcInheritance(self):
346 obj=AbstractBear(Props(1,'bruno'))
347 res=self.pyson.toJson(obj)
348 print("result:"+str(res))
349 bson={'AbstractBear': {'props': {'age': 1, 'name': 'bruno'}}}
350 self.assertEqual(bson, res)
351
352 res=self.pyson.parse(bson, AbstractAnimal)
353 print("Deserialized an Animal! -->"+str(res))
354 self. assertEqual(obj, res)
355
356
357
358 def testUntypedList(self):
359 class Prim:
360 def __init__(self, a:list):
361 self._a=a
362 def geta(self)->list:
363 return self._a
364 def __eq__(self, other):
365 return isinstance(other, self.__class__) and self._a==other._a
366
367 obj=Prim([1,2])
368 objson = {'a':[1,2]}
369
370 self.assertEqual(objson, self.pyson.toJson(obj))
371 self.assertEqual(obj, self.pyson.parse(objson, Prim))
372
373 def testDateTime(self):
374 objson = 1000120 # 1000.12ms since 1970
375 obj=datetime.fromtimestamp(objson/1000.0);
376 self.assertEqual(objson, self.pyson.toJson(obj))
377 self.assertEqual(obj, self.pyson.parse(objson, datetime))
378
379
380 def testTypedList(self):
381 '''
382 deserializes typed list contained in another object
383 '''
384 class Prim:
385 def __init__(self, a:List[str]):
386 self._a=a
387 def geta(self)->List[str]:
388 return self._a
389 def __eq__(self, other):
390 return isinstance(other, self.__class__) and \
391 self._a==other._a
392
393 obj=Prim(["x","y"])
394 objson = {'a':["x","y"]}
395
396 self.assertEqual(objson, self.pyson.toJson(obj))
397 self.assertEqual(obj, self.pyson.parse(objson, Prim))
398
399 def testTypedListDirect(self):
400 '''
401 deserializes typed list directly
402 '''
403
404 obj=["x","y"]
405 objson = ["x","y"]
406
407 self.assertEqual(objson, self.pyson.toJson(obj))
408 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
409
410 def testMixedDict(self):
411 obj={'a':1, 'b':'blabla'}
412
413 # primitive types, should not be changed
414 self.assertEqual(obj, self.pyson.toJson(obj))
415 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
416
417 def testTypedListOfObjMissingAnnotation(self):
418 class Prim:
419 def __init__(self, a:int):
420 self._a=a
421 def geta(self)->int:
422 return self._a
423 def __eq__(self, other):
424 return isinstance(other, self.__class__) and \
425 self._a==other._a
426 obj=[Prim(1),Prim(3)]
427 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
428
429 # SHOULD WE CHECK THIS?
430 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
431 # object misses annotation, therefore this will try to parse
432 # Prim objects without header here.
433 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
434
435 def testTypedListOfObj(self):
436 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
437 class Prim:
438 def __init__(self, a:int):
439 self._a=a
440 def geta(self)->int:
441 return self._a
442 def __eq__(self, other):
443 return isinstance(other, self.__class__) and \
444 self._a==other._a
445
446 obj=[Prim(1),Prim(3)]
447 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
448 self.assertEqual(objson, self.pyson.toJson(obj))
449 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
450
451 def testTypedSetOfObj(self):
452 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
453 class Prim:
454 def __init__(self, a:int):
455 self._a=a
456 def geta(self)->int:
457 return self._a
458 def __eq__(self, other):
459 return isinstance(other, self.__class__) and \
460 self._a==other._a
461
462 obj=set([SimpleWithHash(1),SimpleWithHash(3)])
463 objson = [{"SimpleWithHash":{'a':1}},{"SimpleWithHash":{'a':3}}]
464 self.assertEqual(objson, self.pyson.toJson(obj))
465 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
466 self.assertEqual(obj, parsedobj)
467
468
469 def testExpectListButGiveDict(self):
470 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
471 class Prim:
472 def __init__(self, a:int):
473 self._a=a
474 def geta(self)->int:
475 return self._a
476 def __eq__(self, other):
477 return isinstance(other, self.__class__) and \
478 self._a==other._a
479
480 objson = { 'a':{"Prim":{'a':1}},'c':{"Prim":{'a':3}}}
481 # we request List but obj is a dict.
482 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
483
484 def testSerializeDict(self):
485 obj={'a':Simple(1),'c':Simple(3)}
486 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
487 self.assertEqual(objson, self.pyson.toJson(obj))
488
489
490 def testTypedDictOfObj(self):
491 obj={'a':Simple(1),'c':Simple(3)}
492 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
493 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
494 print("deserialized obj"+str(objson)+"="+str(obj))
495
496 def testTypedDictSimpleKey(self):
497 key=mydict()
498 key["Simple"]={'a':1}
499 # simple is not hashable
500 objson = { key : 'a' }
501 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
502
503 def testTypedDictSimpleKeyHashable(self):
504 # key is now not primitive!
505 obj={SimpleWithHash(1):'a'}
506
507 # simple is not hashable
508 key=mydict()
509 key["SimpleWithHash"]={'a':1}
510 # simple is not hashable
511 objson = { key : 'a' }
512 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
513
514 def testDeserializeBadSubclass(self):
515 objson= { 'BadSubclass':{ 'a':1}}
516 # FIXME the error message is poor in this case.
517 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
518
519
520 def testModuleInsteadOfClassAsSubclasses(self):
521 objson= { 'BadSubclass':{ 'a':1}}
522 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
523
524
525
526 def testJsonGetter(self):
527 class Getter:
528 def __init__(self, a:int):
529 self._a=a
530 @JsonGetter("a")
531 def getValue(self):
532 return self._a
533 def __eq__(self, other):
534 return isinstance(other, self.__class__) and \
535 self._a==other._a
536
537 getter=Getter(17)
538 objson={'a':17}
539 self.assertEqual(objson, self.pyson.toJson(getter))
540 self.assertEqual(getter, self.pyson.parse(objson, Getter))
541
542 def testGetterIgnoresCase(self):
543 class Getter:
544 def __init__(self, value:int):
545 self._a=value
546 def getValue(self):
547 return self._a
548 def __eq__(self, other):
549 return isinstance(other, self.__class__) and \
550 self._a==other._a
551
552 getter=Getter(17)
553 objson={'value':17}
554 self.assertEqual(objson, self.pyson.toJson(getter))
555 self.assertEqual(getter, self.pyson.parse(objson, Getter))
556
557
558 def testJsonValue(self):
559 getter=Snare(17)
560 objson=17
561 print(self.pyson.toJson(getter))
562 self.assertEqual(objson, self.pyson.toJson(getter))
563 self.assertEqual(getter, self.pyson.parse(objson, Snare))
564
565
566 def testJsonValueAsPart(self):
567 csnare=ContainsSnare(Snare(18))
568 objson={"snare":18}
569 print(self.pyson.toJson(csnare))
570 self.assertEqual(objson, self.pyson.toJson(csnare))
571 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
572
573
574 def testUriJsonValue(self):
575 class UriJson:
576 def __init__(self, value:URI):
577 self._a=value
578 @JsonValue()
579 def getValue(self):
580 return self._a
581 def __eq__(self, other):
582 return isinstance(other, self.__class__) and \
583 self._a==other._a
584
585 objson="http://test/"
586 obj=UriJson(URI(objson))
587 self.assertEqual(objson, self.pyson.toJson(obj))
588 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
589
590
591 def testJsonValueInList(self):
592 snare1=Snare(1)
593 snare2=Snare(2)
594 snareList = [snare1, snare2]
595 self.assertEqual([1, 2],self.pyson.toJson(snareList))
596 self.assertEqual(snareList, self.pyson.parse([1,2], List[Snare]))
597
598
599
600
601 def testJsonDeserializerAnnotation(self):
602 self.assertEqual(Custom(Decimal(1)), self.pyson.parse(1.0, Custom))
603 self.assertEqual(1.0, self.pyson.toJson(Custom(Decimal(1))))
604 self.assertEqual(Custom('bla'), self.pyson.parse('bla', Custom))
605 self.assertEqual('bla', self.pyson.toJson(Custom('bla')))
606
607
608 def testDeserializeMissingValue(self):
609 a=DefaultOne()
610 self.assertEqual(1, a.getValue())
611 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
612
613
614 def testDeserializeDefaultNone(self):
615 a=DefaultNone()
616 self.assertEqual(None, a.getValue())
617 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
618
619 def testDeserializeUnion(self):
620 a=WithUnion(None)
621 self.assertEqual(None, a.getValue())
622 self.assertEqual(a, self.pyson.parse({}, WithUnion))
623
624 def testDeserializeOptional(self):
625 NoneType=type(None)
626 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
627 self.assertEqual(None, defaultone)
628
Note: See TracBrowser for help on using the repository browser.