Coverage for Users/vladimirpavlov/PycharmProjects/parameterizable/tests/test_json_processor_round_trips.py: 98%

1"""Round-trip tests for json_processor serialization and deserialization.

2

3 This module tests the complete serialize-deserialize cycle for various

4 object types including primitives, containers, enums, and custom objects.

5"""

6 import json

7 import pytest

8

9 from enum import Enum

10

11 from mixinforge.json_processor import (

12 _to_serializable_dict,

13 _from_serializable_dict,

14 dumpjs,

15 loadjs,

16 _Markers,

17)

18

19

20 class Color(Enum):

21 RED = 1

22 GREEN = 2

23 BLUE = 3

24

25

26 class DictOnly:

27 def __init__(self):

28 self.x = 10

29 self.y = "hi"

30

31

32 class SlotsOnly:

33 __slots__ = ("a", "b")

34

35 def __init__(self, a=1, b=2):

36 self.a = a

37 self.b = b

38

39

40 class BaseSlots:

41 __slots__ = ("base",)

42

43 def __init__(self):

44 self.base = "base"

45

46

47 class Hybrid(BaseSlots):

48 __slots__ = ("__dict__", "s")

49

50 def __init__(self):

51 super().__init__()

52 self.s = 42

53 self.d = "present in __dict__"

54

55

56 class BadStateTuple(BaseSlots):

57 def __getstate__(self):

58 return 1, 2, 3

59

60

61 class GetParams:

62 def __init__(self, a=3, b="z"):

63 self.a = a

64 self.b = b

65

66 def get_params(self):

67 return {"a": self.a, "b": self.b}

68

69

70 class GetState:

71 def __init__(self, v):

72 self._v = v

73

74 def __getstate__(self):

75 return {"v": self._v}

76

77 def __setstate__(self, state):

78 self._v = state["v"]

79

80

81 class SlottedGetstateDict:

82 __slots__ = ("val",)

83

84 def __init__(self, val):

85 self.val = val

86

87 def __getstate__(self):

88 return {"val": self.val}

89

90

91 class StateNoSetState:

92 def __init__(self, a, b):

93 self.a = a

94 self.b = b

95

96 def __getstate__(self):

97 return {"a": 111, "b": 222}

98

99

100 class WithWeakref:

101 __slots__ = ("a", "__weakref__")

102

103 def __init__(self, a=1):

104 self.a = a

105

106

107 def test_round_trip_dict_only():

108 obj = DictOnly()

109 ser = dumpjs(obj)

110 back = loadjs(ser)

111 assert isinstance(back, DictOnly)

112 assert back.x == 10

113 assert back.y == "hi"

114

115

116 def test_round_trip_slotted_with_getstate_dict():

117 obj = SlottedGetstateDict(101)

118 ser = dumpjs(obj)

119 back = loadjs(ser)

120 assert isinstance(back, SlottedGetstateDict)

121 assert back.val == 101

122

123

124 def test_round_trip_hybrid_slots_and_dict():

125 obj = Hybrid()

126 obj.base = "new base"

127 obj.s = 99

128 obj.d = "new dict val"

129 obj.extra = "another dict val"

130

131 ser = dumpjs(obj)

132 back = loadjs(ser)

133

134 assert isinstance(back, Hybrid)

135 assert back.base == "new base"

136 assert back.s == 99

137 assert back.d == "new dict val"

138 assert back.extra == "another dict val"

139

140

141 def test_round_trip_with_weakref():

142 obj = WithWeakref(a=10)

143 ser = dumpjs(obj)

144 back = loadjs(ser)

145 assert isinstance(back, WithWeakref)

146 assert back.a == 10

147

148

149 def test_recreate_from_malformed_tuple_state_raises():

150 obj = BadStateTuple()

151 serialized = {

152 _Markers.MODULE: __name__,

153 _Markers.CLASS: "BadStateTuple",

154 _Markers.STATE: _to_serializable_dict(obj.__getstate__()),

155 }

156

157 with pytest.raises(TypeError, match="Tuple state length .* does not match"):

158 _from_serializable_dict(serialized)

159

160

161 def test_recreate_from_cpython_getstate_tuple_format():

162 """Verify reconstruction from CPython's (dict, slots) tuple state format."""

163 obj = Hybrid()

164 obj.base = "b"

165 obj.s = 1

166 obj.d = 2

167

168 state_tuple = ({"d": 2}, {"base": "b", "s": 1})

169

170 serialized_payload = {

171 _Markers.MODULE: __name__,

172 _Markers.CLASS: "Hybrid",

173 _Markers.STATE: _to_serializable_dict(state_tuple),

174 }

175

176 reconstructed = _from_serializable_dict(serialized_payload)

177

178 assert isinstance(reconstructed, Hybrid)

179 assert reconstructed.base == "b"

180 assert reconstructed.s == 1

181 assert reconstructed.d == 2

182

183

184@pytest.mark.parametrize(

185 "obj, equals",

186 [

187 (None, lambda a, b: a is b),

188 (True, lambda a, b: a is b),

189 (0, lambda a, b: a == b),

190 (3.14, lambda a, b: a == b),

191 ("abc", lambda a, b: a == b),

192 ([1, 2, 3], lambda a, b: a == b),

193 ((1, 2, 3), lambda a, b: a == b),

194 ({1, 2, 3}, lambda a, b: a == b),

195 ({"a": 1, "b": (2, 3)}, lambda a, b: a == b),

196 ({1: "a", 2: "b"}, lambda a, b: a == b),

197 ({"a": 1, 2: "b"}, lambda a, b: a == b),

198 (Color.RED, lambda a, b: a is b),

199 ],

200)

201 def test_round_trip_matrix(obj, equals):

202 ser = _to_serializable_dict(obj)

203 back = _from_serializable_dict(ser)

204 assert equals(obj, back)

205

206

207 def test_from_serializable_round_trip_complex():

208 complex_obj = {

209 (1, 2): {"a": [1, 2, {3, 4}]},

210 "enum": Color.RED,

211 "obj": GetParams(8, "q"),

212 }

213 ser = _to_serializable_dict(complex_obj)

214 de = _from_serializable_dict(ser)

215 assert (1, 2) in de

216 assert de["enum"] is Color.RED

217 assert isinstance(de["obj"], GetParams)

218 assert de["obj"].a == 8 and de["obj"].b == "q"

219 assert de[(1, 2)]["a"][2] == {3, 4}

220

221

222 def test_round_trip_custom_objects_both_variants():

223 gp = GetParams(7, "z")

224 gp_ser = _to_serializable_dict(gp)

225 gp_back = _from_serializable_dict(gp_ser)

226 assert isinstance(gp_back, GetParams)

227 assert gp_back.a == 7 and gp_back.b == "z"

228

229 gs = GetState(123)

230 gs_ser = _to_serializable_dict(gs)

231 gs_back = _from_serializable_dict(gs_ser)

232 assert isinstance(gs_back, GetState)

233 assert gs_back._v == 123

234

235

236 def test_round_trip_slots_only():

237 obj = SlotsOnly(a=10, b=20)

238 ser = dumpjs(obj)

239 back = loadjs(ser)

240 assert isinstance(back, SlotsOnly)

241 assert back.a == 10

242 assert back.b == 20

243

244

245 def test_mixed_key_dict_uses_DICT_marker_and_round_trips():

246 data = {"a": 1, 2: (3, 4)}

247 ser = _to_serializable_dict(data)

248 assert isinstance(ser, dict) and _Markers.DICT in ser

249 back = _from_serializable_dict(ser)

250 assert back == {"a": 1, 2: (3, 4)}

251

252

253@pytest.mark.parametrize(

254 "payload",

255 [

256 {_Markers.TUPLE: [1, 2], "extra": 1},

257 {_Markers.SET: [1, 2], "extra": 1},

258 {_Markers.DICT: [], "extra": 1},

259 ],

260)

261 def test_from_serializable_marker_exclusive_key_violation(payload):

262 with pytest.raises(TypeError):

263 _from_serializable_dict(payload)

264

265

266@pytest.mark.parametrize(

267 "payload",

268 [

269 {_Markers.TUPLE: 123},

270 {_Markers.SET: 123},

271 {_Markers.DICT: 123},

272 {_Markers.DICT: [[1, 2, 3]]},

273 ],

274)

275 def test_from_serializable_marker_value_type_violation(payload):

276 with pytest.raises(TypeError):

277 _from_serializable_dict(payload)

278

279

280 def test_dumps_and_loads_round_trip_json_string_with_kwargs():

281 obj = {

282 "msg": "hello",

283 "nums": (1, 2, 3),

284 "enum": Color.GREEN,

285 "inner": GetParams(2, "ok"),

286 }

287 s = dumpjs(obj, indent=2, sort_keys=True)

288 assert isinstance(s, str) and "\n" in s and " " in s

289

290 loaded = loadjs(s)

291 assert loaded["msg"] == "hello"

292 assert loaded["nums"] == (1, 2, 3)

293 assert loaded["enum"] is Color.GREEN

294 assert isinstance(loaded["inner"], GetParams)

295 assert loaded["inner"].a == 2

296 assert loaded["inner"].b == "ok"

297

298

299 def test_loads_forbids_object_hook_and_invalid_json():

300 with pytest.raises(ValueError):

301 loadjs("{}", object_hook=lambda d: d)

302 with pytest.raises(json.JSONDecodeError):

303 loadjs("not a json")