from __future__ import annotations from abc import ABC from datetime import datetime from decimal import Decimal import json import re import sys, traceback from typing import Dict, List, Set, Any, Union, Optional, Collection import unittest from uuid import uuid4, UUID from pyson.Deserializer import Deserializer from pyson.Serializer import Serializer from pyson.JsonSerialize import JsonSerialize from pyson.JsonDeserialize import JsonDeserialize from pyson.JsonGetter import JsonGetter from pyson.JsonSubTypes import JsonSubTypes from pyson.JsonTypeInfo import Id, As from pyson.JsonTypeInfo import JsonTypeInfo from pyson.JsonValue import JsonValue from pyson.ObjectMapper import ObjectMapper from pickle import NONE from test.MyDeserializer import ValueDeserializer2 from cgitb import reset class SuperSimple: def __init__(self, a:Optional[int]): self.a=a def __eq__(self, other): return isinstance(other, self.__class__) and \ self.a==other.a def getA(self): return self.a def __repr__(self): return "SuperSimple::"+str(self.a) def __hash__(self): return hash(self.a) # deserializer DROPS FIRST CHAR from string and assumes rest is an int. # Then returns Simple(int) class ValueDeserializer(Deserializer): def __hash__(self): return hash(self.geta()) def deserialize(self, data:object, clas: object)-> object: if type(data)!=str: raise ValueError("Expected str starting with '$', got "+str(data)) return Simple(int(data[1:])) # serializes Simple object, just prefixing its value (as string) with "$" class ValueSerializer(Serializer): def serialize(self, obj:object)-> object: if not isinstance(obj, Simple): raise ValueError("Expected Dimple object") return "$" + str(obj.geta()) class Basic: def __init__(self, v:float): self._v=v def __eq__(self, other): return isinstance(other, self.__class__) and \ self._v==other._v def getV(self): return self._v def __repr__(self): return "Basic:"+str(self._v) def __hash__(self): return hash(self._v) @JsonDeserialize(ValueDeserializer) @JsonSerialize(ValueSerializer) class Simple: def __init__(self, a:int): self._a=a def geta(self)->int: return self._a def __eq__(self, other): return isinstance(other, self.__class__) and \ self._a==other._a def __repr__(self): return "Simple:"+str(self._a) def __hash__(self): return hash(self._a) @JsonDeserialize(ValueDeserializer2) class Simple2: def __init__(self, a:int): self._a=a def geta(self)->int: return self._a def __eq__(self, other): return isinstance(other, self.__class__) and \ self._a==other._a def __repr__(self): return self._name+","+str(self._a) #None cancels out the existing deserializer. # parsing with str should now fail. @JsonDeserialize(None) class Simple3 (Simple): pass class MyList: def __init__(self, data: List[Simple] ): self.data=data def getData(self)->List[Simple]: return self.data class BasicDict: def __init__(self, data: Dict[Basic,Basic] ): self.data=data def getData(self)->Dict[Basic,Basic]: return self.data def __repr__(self): return "BasicDict:"+str(self.data) def __eq__(self, other): return isinstance(other, self.__class__) and \ self.data==other.data class SimpleDict: def __init__(self, data: Dict[Simple,Simple] ): self.data=data def getData(self)->Dict[Simple,Simple]: return self.data def __eq__(self, other): return isinstance(other, self.__class__) and \ self.data==other.data def __repr__(self): return "SimpleDict:"+str(self.data) class MyOptionalString: def __init__(self, s:Optional[str]): self.data:Optional[str] = s def getData(self)->Optional[str]: return self.data def __eq__(self, other): return isinstance(other, self.__class__) and \ self.data==other.data def __repr__(self): return "MyOptionalString:"+str(self.data) @JsonSubTypes(['test.DeserializerTest.A','test.DeserializerTest.B','test.DeserializerTest.C']) @JsonTypeInfo(use=Id.NAME, include=As.WRAPPER_OBJECT) class Root: def __init__(self, data: int ): self.data:int=data def getData(self)->int: return self.data def __eq__(self, other): return isinstance(other, self.__class__) and \ self.data==other.data def __repr__(self): return type(self).__name__ + "/" + str(self.data) def __hash__(self): return 0 class A(Root): pass class B(Root): pass class C(Root): def __init__(self, data:int, name: str): super().__init__(data) self.name = name def __eq__(self, other): return isinstance(other, self.__class__) and \ self.data==other.data and self.name==other.name def __repr__(self): return super().__repr__()+","+self.name class BaseValueDeserializer (Deserializer): def deserialize(self, data:object, clas: object) -> 'BaseValue': if isinstance(data,str): return BaseDiscreteValue(data) raise ValueError("Expected number or double quoted string but found " + str(data) + " of type " + str(type(data))) @JsonDeserialize(using=BaseValueDeserializer) class BaseValue(ABC): def __init__(self, value): self._value = value; @JsonValue() def getValue(self) -> str: return self._value; #disable parent deserializer. This is essential #331 #@JsonDeserialize(using=None) class BaseDiscreteValue(BaseValue): def __init__(self, value:str): self.__value=value @JsonValue() def getValue(self)->str: return self.__value class DeserializerTest(unittest.TestCase): ''' Test a lot of back-and-forth cases. FIXME Can we make this a parameterized test? ''' pyson=ObjectMapper() mixedList:List[Root] = [A(1), B(1)] mixedlistjson:List = [{'A': {'data': 1}}, {'B': {'data': 1}}] mixedDict:Dict[Root,int] = {A(1):1, B(1):2} mixedDictJson:dict = {'{"A": {"data": 1}}': 1, '{"B": {"data": 1}}': 2} MyC:C = C(3, "MyC") def testDeserialize(self): objson= "$12" self.assertEqual(Simple(12), self.pyson.parse(objson, Simple)) def testExternalDeserialize2(self): objson= "$13" self.assertEqual(13, self.pyson.parse(objson, Simple2)) def testExternalDeserialize3(self): objson= "$13" self.assertRaises(ValueError, lambda:self.pyson.parse(objson, Simple3)) def testDeserializeMyList(self): print(self.pyson.toJson(MyList([12,13]))) # the json we provide is NOT the proper json but a STRING. # This triggers a fallback mechanism that tries to parse the string as json. objson={"data": ["$12", "$13"]} res = self.pyson.parse(objson, MyList) print(res) self.assertEqual([Simple(12),Simple(13)], res.data) def testDeserializeCollection(self): objson=[1,2,3] # this checks that pyson can parse a list as Collection. # The result is a simple list like in java/jackson. # Collection without typing info means Collectino[Any] res = self.pyson.parse(objson, Collection) print(res) self.assertEqual([1,2,3], res) def testSerializeBasicDict(self): ''' Basic object keys. Special (de)serializer should kick in #190 ''' d= BasicDict( { Basic(1.):Basic(2.), Basic(3.): Basic(4.) } ) objson={"data": {"{\"v\": 1.0}": {"v": 2.0}, "{\"v\": 3.0}": {"v": 4.0}}} dump=json.dumps(self.pyson.toJson(d)); print(dump) # self.assertEqual(objson, dump); res=self.pyson.parse(objson, BasicDict) print("res="+str(res)) self.assertEqual(d, res) def testSerializeSimpleDictCustomSerializer(self): d= SimpleDict( { Simple(1):Simple(2), Simple(3): Simple(4) } ) # The keys need extra quotes, they are deserialied by # our special deserializer that parses the string as json, # and json requires double quotes around its strings. objson = {"data": {'"$1"': "$2", '"$3"': "$4"}} obj=self.pyson.toJson(d) print(json.dumps(obj)) self.assertEqual(objson, obj) res = self.pyson.parse(objson, SimpleDict) print("res="+str(res)) self.assertEqual(d, res) def testDeserializeOptString(self): objson:dict={'s':None} res = self.pyson.parse(objson, MyOptionalString) print(res) self.assertEqual(MyOptionalString(None), res) objson={'s':"something"} res = self.pyson.parse(objson, MyOptionalString) print(res) self.assertEqual(MyOptionalString("something"), res) def testSerializeMixedList(self): # see #296 res=self.pyson.toJson(self.mixedList) print(res) self.assertEqual(self.mixedlistjson, res) def testDeserializeMixedList(self): # see #296 res=self.pyson.parse(self.mixedlistjson, List[Root]) print(res) self.assertEqual(self.mixedList,res) def testDeserializeBadList(self): # see #298. This SHOULD fail because B is not subtype of A self.assertRaises(ValueError, lambda:self.pyson.parse(self.mixedlistjson, List[A])) def testSerializeMixedDict(self): # see #296 res=self.pyson.toJson(self.mixedDict) print(res) self.assertEqual(self.mixedDictJson, res) def testDeserializeMixedDict(self): # see #296 print("testDeserializeMixedDict") res=self.pyson.parse(self.mixedDictJson, Dict[Root,int]) print(res) self.assertEqual(self.mixedDict,res) def testDeserializeC(self): # see #298. Sub-class should not expect res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, Root) print(reset) self.assertEqual(self.MyC, res) # even if you serialize as C, you still need the 'C' wrapper res=self.pyson.parse({'C':{'data':3, 'name':'MyC' }}, C) self.assertEqual(self.MyC, res) def testDeserializeBaseValue(self): # see #331 res=self.pyson.parse({'yes': 1, 'no': 0}, Dict[BaseDiscreteValue, int]) print(res) def testDeserializeSuperSimple(self): res=self.pyson.parse({'a':1 } , SuperSimple) print(res) self.assertEqual(SuperSimple(1), res) def testDeserializeMissingValue(self): res=self.pyson.parse({ } , SuperSimple) # missing value for a print(res) self.assertEqual(SuperSimple(None), res) def testDeserializeStandardDict(self): val:Dict = self.pyson.parse(json.loads("{\"a\":0.3,\"b\":{\"x\":3},\"c\":[1,2,3]}"),Dict)