source: pyson/test/ObjectMapperTest.py@ 311

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

#104 added test showing round error

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