source: pyson/test/ObjectMapperTest.py@ 193

Last change on this file since 193 was 193, checked in by wouter, 3 years ago

#84 use int instead of float for Decimals containing integral value

File size: 19.6 KB
Line 
1
2from datetime import datetime
3import sys, traceback
4
5from uri import URI
6
7from abc import ABC
8from decimal import Decimal
9import json
10from pyson.Deserializer import Deserializer
11from pyson.JsonDeserialize import JsonDeserialize
12from pyson.JsonGetter import JsonGetter
13from pyson.JsonSubTypes import JsonSubTypes
14from pyson.JsonTypeInfo import Id, As
15from pyson.JsonTypeInfo import JsonTypeInfo
16from pyson.JsonValue import JsonValue
17from pyson.ObjectMapper import ObjectMapper
18import re
19from typing import Dict, List, Set, Any, Union
20import unittest
21from uuid import uuid4, UUID
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. assertEquals(1200, v)
256
257 def testDecimalSerializationType(self):
258 v=self.pyson.toJson(Decimal('1200'))
259 self.assertEquals(int, type(v))
260
261 v=self.pyson.toJson(Decimal('1200.1'))
262 self.assertEquals(float, type(v))
263
264
265 def testPrimitiveUUID(self):
266 id=uuid4()
267 self.assertEqual(id, self.pyson.parse(str(id), UUID))
268 self.assertEqual(str(id), self.pyson.toJson(id))
269
270 def testProps(self):
271 propsjson={'age': 10, 'name': 'pietje'}
272 props=Props(10, "pietje")
273 self.assertEqual(propsjson,self.pyson.toJson(props))
274 self.assertEqual(props, self.pyson.parse(propsjson, Props))
275
276 def testParseDeepError(self):
277 propsjson={'age': 10, 'name': 12}
278 try:
279 self.pyson.parse(propsjson, Props)
280 raise AssertionError("parser did not throw")
281 except ValueError as e:
282 # we catch this to assure the exception contains
283 # both top error and details.
284 print("received error "+str(e))
285 self.assertTrue(str(e).find("Error parsing"))
286 self.assertTrue(str(e).find("ValueError"))
287 self.assertTrue(str(e).find("expected"))
288
289
290 def testEmpty(self):
291
292 class EmptyClass:
293 def __init__(self):
294 pass
295 def __eq__(self, other):
296 return isinstance(other, self.__class__)
297
298 obj=EmptyClass()
299 print(self.pyson.toJson(obj))
300 res=self.pyson.parse({}, EmptyClass)
301 self.assertEqual(obj, res)
302
303 def testSubType(self):
304
305 class Cat():
306 def __init__(self, props:Props):
307 self._props=props
308
309 def __str__(self):
310 return "Cat["+str(self._props)+"]"
311
312 def getprops(self):
313 return self._props
314
315 obj=Cat(Props(1,'bruno'))
316 print(self.pyson.toJson(obj))
317
318
319 bson={'props':{'age':1, 'name':'bruno'}}
320 res=self.pyson.parse(bson, Cat)
321 print(res, type(res))
322 self.assertEqual(type(res.getprops()), Props)
323
324 def testDeserializeNoSuchField(self):
325 # Bear has a 'props' field, bot 'b'
326 self.assertRaises(ValueError, lambda:self.pyson.parse({'b':1}, Props))
327 #self.pyson.parse({'b':1}, Props)
328
329 def testInheritance(self):
330 obj=Bear(Props(1,'bruno'))
331 res=self.pyson.toJson(obj)
332 print("result:"+str(res))
333 bson={'Bear': {'props': {'age': 1, 'name': 'bruno'}}}
334 self.assertEqual(bson, res)
335
336 res=self.pyson.parse(bson, Animal)
337 print("Deserialized an Animal! -->"+str(res))
338 self. assertEqual(obj, res)
339
340 def testAbcInheritance(self):
341 obj=AbstractBear(Props(1,'bruno'))
342 res=self.pyson.toJson(obj)
343 print("result:"+str(res))
344 bson={'AbstractBear': {'props': {'age': 1, 'name': 'bruno'}}}
345 self.assertEqual(bson, res)
346
347 res=self.pyson.parse(bson, AbstractAnimal)
348 print("Deserialized an Animal! -->"+str(res))
349 self. assertEqual(obj, res)
350
351
352
353 def testUntypedList(self):
354 class Prim:
355 def __init__(self, a:list):
356 self._a=a
357 def geta(self)->list:
358 return self._a
359 def __eq__(self, other):
360 return isinstance(other, self.__class__) and self._a==other._a
361
362 obj=Prim([1,2])
363 objson = {'a':[1,2]}
364
365 self.assertEqual(objson, self.pyson.toJson(obj))
366 self.assertEqual(obj, self.pyson.parse(objson, Prim))
367
368 def testDateTime(self):
369 objson = 1000120 # 1000.12ms since 1970
370 obj=datetime.fromtimestamp(objson/1000.0);
371 self.assertEqual(objson, self.pyson.toJson(obj))
372 self.assertEqual(obj, self.pyson.parse(objson, datetime))
373
374
375 def testTypedList(self):
376 '''
377 deserializes typed list contained in another object
378 '''
379 class Prim:
380 def __init__(self, a:List[str]):
381 self._a=a
382 def geta(self)->List[str]:
383 return self._a
384 def __eq__(self, other):
385 return isinstance(other, self.__class__) and \
386 self._a==other._a
387
388 obj=Prim(["x","y"])
389 objson = {'a':["x","y"]}
390
391 self.assertEqual(objson, self.pyson.toJson(obj))
392 self.assertEqual(obj, self.pyson.parse(objson, Prim))
393
394 def testTypedListDirect(self):
395 '''
396 deserializes typed list directly
397 '''
398
399 obj=["x","y"]
400 objson = ["x","y"]
401
402 self.assertEqual(objson, self.pyson.toJson(obj))
403 self.assertEqual(obj, self.pyson.parse(objson, List[str]))
404
405 def testMixedDict(self):
406 obj={'a':1, 'b':'blabla'}
407
408 # primitive types, should not be changed
409 self.assertEqual(obj, self.pyson.toJson(obj))
410 self.assertEqual(obj, self.pyson.parse(obj, Dict[Any,Any]))
411
412 def testTypedListOfObjMissingAnnotation(self):
413 class Prim:
414 def __init__(self, a:int):
415 self._a=a
416 def geta(self)->int:
417 return self._a
418 def __eq__(self, other):
419 return isinstance(other, self.__class__) and \
420 self._a==other._a
421 obj=[Prim(1),Prim(3)]
422 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
423
424 # SHOULD WE CHECK THIS?
425 #self.assertRaises(ValueError, lambda:self.pyson.toJson(obj))
426 # object misses annotation, therefore this will try to parse
427 # Prim objects without header here.
428 self.assertRaises(ValueError, lambda:self.pyson.parse(objson, List[Prim]))
429
430 def testTypedListOfObj(self):
431 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
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
441 obj=[Prim(1),Prim(3)]
442 objson = [{"Prim":{'a':1}},{"Prim":{'a':3}}]
443 self.assertEqual(objson, self.pyson.toJson(obj))
444 self.assertEqual(obj, self.pyson.parse(objson, List[Prim]))
445
446 def testTypedSetOfObj(self):
447 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
448 class Prim:
449 def __init__(self, a:int):
450 self._a=a
451 def geta(self)->int:
452 return self._a
453 def __eq__(self, other):
454 return isinstance(other, self.__class__) and \
455 self._a==other._a
456
457 obj=set([SimpleWithHash(1),SimpleWithHash(3)])
458 objson = [{"SimpleWithHash":{'a':1}},{"SimpleWithHash":{'a':3}}]
459 self.assertEqual(objson, self.pyson.toJson(obj))
460 parsedobj=self.pyson.parse(objson, Set[SimpleWithHash])
461 self.assertEqual(obj, parsedobj)
462
463
464 def testExpectListButGiveDict(self):
465 @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT)
466 class Prim:
467 def __init__(self, a:int):
468 self._a=a
469 def geta(self)->int:
470 return self._a
471 def __eq__(self, other):
472 return isinstance(other, self.__class__) and \
473 self._a==other._a
474
475 objson = { 'a':{"Prim":{'a':1}},'c':{"Prim":{'a':3}}}
476 # we request List but obj is a dict.
477 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, List[Prim]))
478
479 def testSerializeDict(self):
480 obj={'a':Simple(1),'c':Simple(3)}
481 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
482 self.assertEqual(objson, self.pyson.toJson(obj))
483
484
485 def testTypedDictOfObj(self):
486 obj={'a':Simple(1),'c':Simple(3)}
487 objson = { 'a':{"Simple":{'a':1}},'c':{"Simple":{'a':3}}}
488 self.assertEqual(obj, self.pyson.parse(objson, Dict[str,Simple]))
489 print("deserialized obj"+str(objson)+"="+str(obj))
490
491 def testTypedDictSimpleKey(self):
492 key=mydict()
493 key["Simple"]={'a':1}
494 # simple is not hashable
495 objson = { key : 'a' }
496 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, Dict[Simple,str]))
497
498 def testTypedDictSimpleKeyHashable(self):
499 # key is now not primitive!
500 obj={SimpleWithHash(1):'a'}
501
502 # simple is not hashable
503 key=mydict()
504 key["SimpleWithHash"]={'a':1}
505 # simple is not hashable
506 objson = { key : 'a' }
507 self.assertEqual(obj, self.pyson.parse(objson, Dict[SimpleWithHash,str]))
508
509 def testDeserializeBadSubclass(self):
510 objson= { 'BadSubclass':{ 'a':1}}
511 # FIXME the error message is poor in this case.
512 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassMissingTypeInfo))
513
514
515 def testModuleInsteadOfClassAsSubclasses(self):
516 objson= { 'BadSubclass':{ 'a':1}}
517 self.assertRaises(ValueError,lambda:self.pyson.parse(objson, BadSuperclassModuleInstead))
518
519
520
521 def testJsonGetter(self):
522 class Getter:
523 def __init__(self, a:int):
524 self._a=a
525 @JsonGetter("a")
526 def getValue(self):
527 return self._a
528 def __eq__(self, other):
529 return isinstance(other, self.__class__) and \
530 self._a==other._a
531
532 getter=Getter(17)
533 objson={'a':17}
534 self.assertEqual(objson, self.pyson.toJson(getter))
535 self.assertEqual(getter, self.pyson.parse(objson, Getter))
536
537 def testGetterIgnoresCase(self):
538 class Getter:
539 def __init__(self, value:int):
540 self._a=value
541 def getValue(self):
542 return self._a
543 def __eq__(self, other):
544 return isinstance(other, self.__class__) and \
545 self._a==other._a
546
547 getter=Getter(17)
548 objson={'value':17}
549 self.assertEqual(objson, self.pyson.toJson(getter))
550 self.assertEqual(getter, self.pyson.parse(objson, Getter))
551
552
553 def testJsonValue(self):
554 getter=Snare(17)
555 objson=17
556 print(self.pyson.toJson(getter))
557 self.assertEqual(objson, self.pyson.toJson(getter))
558 self.assertEqual(getter, self.pyson.parse(objson, Snare))
559
560
561 def testJsonValueAsPart(self):
562 csnare=ContainsSnare(Snare(18))
563 objson={"snare":18}
564 print(self.pyson.toJson(csnare))
565 self.assertEqual(objson, self.pyson.toJson(csnare))
566 self.assertEqual(csnare, self.pyson.parse(objson, ContainsSnare))
567
568
569 def testUriJsonValue(self):
570 class UriJson:
571 def __init__(self, value:URI):
572 self._a=value
573 @JsonValue()
574 def getValue(self):
575 return self._a
576 def __eq__(self, other):
577 return isinstance(other, self.__class__) and \
578 self._a==other._a
579
580 objson="http://test/"
581 obj=UriJson(URI(objson))
582 self.assertEqual(objson, self.pyson.toJson(obj))
583 self.assertEqual(obj, self.pyson.parse(objson, UriJson))
584
585
586 def testJsonValueInList(self):
587 snare1=Snare(1)
588 snare2=Snare(2)
589 snareList = [snare1, snare2]
590 self.assertEqual([1, 2],self.pyson.toJson(snareList))
591 self.assertEqual(snareList, self.pyson.parse([1,2], List[Snare]))
592
593
594
595
596 def testJsonDeserializerAnnotation(self):
597 self.assertEqual(Custom(Decimal(1)), self.pyson.parse(1.0, Custom))
598 self.assertEqual(1.0, self.pyson.toJson(Custom(Decimal(1))))
599 self.assertEqual(Custom('bla'), self.pyson.parse('bla', Custom))
600 self.assertEqual('bla', self.pyson.toJson(Custom('bla')))
601
602
603 def testDeserializeMissingValue(self):
604 a=DefaultOne()
605 self.assertEqual(1, a.getValue())
606 self.assertEqual(a, self.pyson.parse({}, DefaultOne))
607
608
609 def testDeserializeDefaultNone(self):
610 a=DefaultNone()
611 self.assertEqual(None, a.getValue())
612 self.assertEqual(a, self.pyson.parse({}, DefaultNone))
613
614 def testDeserializeUnion(self):
615 a=WithUnion(None)
616 self.assertEqual(None, a.getValue())
617 self.assertEqual(a, self.pyson.parse({}, WithUnion))
618
619 def testDeserializeOptional(self):
620 NoneType=type(None)
621 defaultone= self.pyson.parse(None, Union[DefaultOne, NoneType])
622 self.assertEqual(None, defaultone)
623
Note: See TracBrowser for help on using the repository browser.