source: pyson/test/ObjectMapperTest.py@ 188

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

#79 parse null issue

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