Coverage for Users/vladimirpavlov/PycharmProjects/parameterizable/tests/test_guarded_init_metaclass.py: 99%

1 import pytest

2 import pickle

3 from dataclasses import dataclass

4 from mixinforge import GuardedInitMeta

5

6# --- Helper classes for pickling tests (must be at module level) ---

7

8 class PickleClass(metaclass=GuardedInitMeta):

9 def __init__(self, value):

10 self._init_finished = False

11 self.value = value

12

13 def __getstate__(self):

14 state = self.__dict__.copy()

15 if "_init_finished" in state:

16 del state["_init_finished"]

17 return state

18

19 class BadPickleClass(metaclass=GuardedInitMeta):

20 def __init__(self):

21 self._init_finished = False

22

23 class PostSetStateClass(metaclass=GuardedInitMeta):

24 def __init__(self):

25 self._init_finished = False

26 self.restored = False

27

28 def __getstate__(self):

29 state = self.__dict__.copy()

30 state.pop('_init_finished', None)

31 return state

32

33 def __post_setstate__(self):

34 self.restored = True

35

36 class ErrorPostSetStateClass(metaclass=GuardedInitMeta):

37 def __init__(self):

38 self._init_finished = False

39

40 def __getstate__(self):

41 state = self.__dict__.copy()

42 state.pop('_init_finished', None)

43 return state

44

45 def __post_setstate__(self):

46 raise ValueError("Restoration failed")

47

48# --- New Helper Classes ---

49

50 class ParentWithSetState(metaclass=GuardedInitMeta):

51 def __init__(self):

52 self._init_finished = False

53 def __getstate__(self):

54 d = self.__dict__.copy()

55 d.pop('_init_finished', None)

56 return d

57 def __setstate__(self, state):

58 self.__dict__.update(state)

59 self.setstate_called = True

60

61 class ChildInheritsSetState(ParentWithSetState):

62 pass

63

64 class ClassDictOnly(metaclass=GuardedInitMeta):

65 def __init__(self, value):

66 self._init_finished = False

67 self.value = value

68 def __getstate__(self):

69 d = self.__dict__.copy()

70 d.pop('_init_finished', None)

71 return d

72

73 class ClassSlotsOnly(metaclass=GuardedInitMeta):

74 __slots__ = ('value', '_init_finished')

75 def __init__(self, value):

76 self._init_finished = False

77 self.value = value

78 def __getstate__(self):

79 return (None, {'value': self.value})

80

81 class ClassDictAndSlots(metaclass=GuardedInitMeta):

82 __slots__ = ('s_val', '_init_finished', '__dict__')

83 def __init__(self, d_val, s_val):

84 self._init_finished = False

85 self.d_val = d_val

86 self.s_val = s_val

87 def __getstate__(self):

88 d = self.__dict__.copy()

89 d.pop('_init_finished', None)

90 return (d, {'s_val': self.s_val})

91

92 class FactoryClass(metaclass=GuardedInitMeta):

93 def __new__(cls):

94 return {"not": "instance"}

95 def __init__(self):

96 self._init_finished = False

97

98 class BadPostInitClass(metaclass=GuardedInitMeta):

99 __post_init__ = 123

100 def __init__(self):

101 self._init_finished = False

102

103 class BadPostSetStateClass(metaclass=GuardedInitMeta):

104 __post_setstate__ = "foo"

105 def __init__(self):

106 self._init_finished = False

107 def __getstate__(self):

108 d = self.__dict__.copy()

109 d.pop('_init_finished', None)

110 return d

111

112 class SlotsMismatchClass(metaclass=GuardedInitMeta):

113 __slots__ = ('x', '_init_finished')

114 def __init__(self):

115 self._init_finished = False

116 def __getstate__(self):

117 # Return a dict to trigger the mismatch error during setstate

118 return {'x': 1}

119

120# --- Tests ---

121

122 def test_basic_initialization():

123 """Test that initialization works correctly when contract is followed."""

124 class GoodClass(metaclass=GuardedInitMeta):

125 def __init__(self):

126 self._init_finished = False

127 self.value = 10

128

129 obj = GoodClass()

130 assert obj._init_finished is True

131 assert obj.value == 10

132

133 def test_missing_init_flag():

134 """Test that RuntimeError is raised if _init_finished is not set to False in __init__."""

135 class BadClass(metaclass=GuardedInitMeta):

136 def __init__(self):

137 self.value = 10

138 # Missing self._init_finished = False

139

140 with pytest.raises(RuntimeError, match="must set attribute _init_finished to False"):

141 BadClass()

142

143 def test_post_init_hook():

144 """Test that __post_init__ is called."""

145 class PostInitClass(metaclass=GuardedInitMeta):

146 def __init__(self):

147 self._init_finished = False

148 self.initialized_count = 0

149

150 def __post_init__(self):

151 self.initialized_count += 1

152

153 obj = PostInitClass()

154 assert obj._init_finished is True

155 assert obj.initialized_count == 1

156

157 def test_post_init_error():

158 """Test that errors in __post_init__ are re-raised with context."""

159 class ErrorPostInitClass(metaclass=GuardedInitMeta):

160 def __init__(self):

161 self._init_finished = False

162

163 def __post_init__(self):

164 raise ValueError("Something went wrong")

165

166 with pytest.raises(ValueError, match="Error in __post_init__"):

167 ErrorPostInitClass()

168

169 def test_dataclass_rejection():

170 """Test that applying GuardedInitMeta to a dataclass raises TypeError on instantiation."""

171 # Note: definition succeeds because is_dataclass is false during __init__

172 @dataclass

173 class MyDataclass(metaclass=GuardedInitMeta):

174 x: int

175

176 with pytest.raises(TypeError, match="GuardedInitMeta cannot be used with dataclass"):

177 MyDataclass(10)

178

179 def test_pickle_success():

180 """Test successful pickle/unpickle cycle with proper __getstate__."""

181 obj = PickleClass(42)

182 assert obj._init_finished is True

183

184 data = pickle.dumps(obj)

185 new_obj = pickle.loads(data)

186

187 assert new_obj.value == 42

188 assert new_obj._init_finished is True

189 assert isinstance(new_obj, PickleClass)

190

191 def test_pickle_failure_if_init_finished_present():

192 """Test that unpickling fails if _init_finished=True is present in state."""

193 obj = BadPickleClass()

194 # Default pickling includes _init_finished=True

195 data = pickle.dumps(obj)

196

197 with pytest.raises(RuntimeError, match="must not be pickled with _init_finished=True"):

198 pickle.loads(data)

199

200 def test_post_setstate_hook():

201 """Test that __post_setstate__ is called after unpickling."""

202 obj = PostSetStateClass()

203 data = pickle.dumps(obj)

204 new_obj = pickle.loads(data)

205

206 assert new_obj.restored is True

207 assert new_obj._init_finished is True

208

209 def test_post_setstate_error():

210 """Test that errors in __post_setstate__ are re-raised with context."""

211 obj = ErrorPostSetStateClass()

212 data = pickle.dumps(obj)

213

214 with pytest.raises(ValueError, match="Error in __post_setstate__"):

215 pickle.loads(data)

216

217# --- New Tests ---

218

219 def test_inherited_setstate_wrapped_once():

220 """Verify inherited __setstate__ is wrapped only once and behaves correctly."""

221 obj = ChildInheritsSetState()

222 data = pickle.dumps(obj)

223 restored = pickle.loads(data)

224

225 assert restored._init_finished is True

226 assert getattr(restored, 'setstate_called', False) is True

227 # Verify object identity of the method

228 assert ChildInheritsSetState.__setstate__ is ParentWithSetState.__setstate__

229

230@pytest.mark.parametrize("cls, init_args, check_fn", [

231 (ClassDictOnly, (10,), lambda o: o.value == 10),

232 (ClassSlotsOnly, (20,), lambda o: o.value == 20),

233 (ClassDictAndSlots, (30, 40), lambda o: o.d_val == 30 and o.s_val == 40),

234])

235 def test_default_restore_paths(cls, init_args, check_fn):

236 """Cover default restore paths when no __setstate__ is present."""

237 obj = cls(*init_args)

238 assert obj._init_finished is True

239

240 data = pickle.dumps(obj)

241 restored = pickle.loads(data)

242

243 assert restored._init_finished is True

244 assert check_fn(restored)

245

246 def test_new_returns_non_instance():

247 """Ensure lifecycle hooks are skipped when __new__ returns a non-instance."""

248 obj = FactoryClass()

249 assert isinstance(obj, dict)

250 assert not hasattr(obj, "_init_finished")

251

252 def test_reject_non_callable_hooks():

253 """Reject non-callable hooks early."""

254 with pytest.raises(TypeError, match="__post_init__ must be callable"):

255 BadPostInitClass()

256

257 obj = BadPostSetStateClass()

258 data = pickle.dumps(obj)

259 with pytest.raises(TypeError, match="__post_setstate__ must be callable"):

260 pickle.loads(data)

261

262 def test_slots_mismatch_guard():

263 """Slots mismatch guard raises RuntimeError."""

264 obj = SlotsMismatchClass()

265 data = pickle.dumps(obj)

266 with pytest.raises(RuntimeError, match="instance has no __dict__"):

267 pickle.loads(data)

268

269 def test_dataclass_definition_rejection():

270 """Dataclass rejection at class-definition time."""

271 @dataclass

272 class BaseDataclass:

273 x: int

274

275 with pytest.raises(TypeError, match="GuardedInitMeta cannot be used with dataclass"):

276 class Child(BaseDataclass, metaclass=GuardedInitMeta):

277 pass