source: pyson/test/ObjectMapperTest.py@ 233

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

fixing type errors

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