Coverage for Users / vladimirpavlov / PycharmProjects / parameterizable / tests / test_guarded_init_metaclass.py: 99%
195 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-01 16:37 -0600
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-01 16:37 -0600
1import pytest
2import pickle
3from dataclasses import dataclass
4from mixinforge import GuardedInitMeta
6# --- Helper classes for pickling tests (must be at module level) ---
8class PickleClass(metaclass=GuardedInitMeta):
9 def __init__(self, value):
10 self._init_finished = False
11 self.value = value
13 def __getstate__(self):
14 state = self.__dict__.copy()
15 if "_init_finished" in state:
16 del state["_init_finished"]
17 return state
19class BadPickleClass(metaclass=GuardedInitMeta):
20 def __init__(self):
21 self._init_finished = False
23class PostSetStateClass(metaclass=GuardedInitMeta):
24 def __init__(self):
25 self._init_finished = False
26 self.restored = False
28 def __getstate__(self):
29 state = self.__dict__.copy()
30 state.pop('_init_finished', None)
31 return state
33 def __post_setstate__(self):
34 self.restored = True
36class ErrorPostSetStateClass(metaclass=GuardedInitMeta):
37 def __init__(self):
38 self._init_finished = False
40 def __getstate__(self):
41 state = self.__dict__.copy()
42 state.pop('_init_finished', None)
43 return state
45 def __post_setstate__(self):
46 raise ValueError("Restoration failed")
48# --- New Helper Classes ---
50class 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
61class ChildInheritsSetState(ParentWithSetState):
62 pass
64class 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
73class 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})
81class 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})
92class FactoryClass(metaclass=GuardedInitMeta):
93 def __new__(cls):
94 return {"not": "instance"}
95 def __init__(self):
96 self._init_finished = False
98class BadPostInitClass(metaclass=GuardedInitMeta):
99 __post_init__ = 123
100 def __init__(self):
101 self._init_finished = False
103class 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
112class 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}
120# --- Tests ---
122def 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
129 obj = GoodClass()
130 assert obj._init_finished is True
131 assert obj.value == 10
133def 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
140 with pytest.raises(RuntimeError, match="must set attribute _init_finished to False"):
141 BadClass()
143def 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
150 def __post_init__(self):
151 self.initialized_count += 1
153 obj = PostInitClass()
154 assert obj._init_finished is True
155 assert obj.initialized_count == 1
157def 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
163 def __post_init__(self):
164 raise ValueError("Something went wrong")
166 with pytest.raises(ValueError, match="Error in __post_init__"):
167 ErrorPostInitClass()
169def 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
176 with pytest.raises(TypeError, match="GuardedInitMeta cannot be used with dataclass"):
177 MyDataclass(10)
179def test_pickle_success():
180 """Test successful pickle/unpickle cycle with proper __getstate__."""
181 obj = PickleClass(42)
182 assert obj._init_finished is True
184 data = pickle.dumps(obj)
185 new_obj = pickle.loads(data)
187 assert new_obj.value == 42
188 assert new_obj._init_finished is True
189 assert isinstance(new_obj, PickleClass)
191def 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)
197 with pytest.raises(RuntimeError, match="must not be pickled with _init_finished=True"):
198 pickle.loads(data)
200def 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)
206 assert new_obj.restored is True
207 assert new_obj._init_finished is True
209def test_post_setstate_error():
210 """Test that errors in __post_setstate__ are re-raised with context."""
211 obj = ErrorPostSetStateClass()
212 data = pickle.dumps(obj)
214 with pytest.raises(ValueError, match="Error in __post_setstate__"):
215 pickle.loads(data)
217# --- New Tests ---
219def 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)
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__
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])
235def 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
240 data = pickle.dumps(obj)
241 restored = pickle.loads(data)
243 assert restored._init_finished is True
244 assert check_fn(restored)
246def 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")
252def test_reject_non_callable_hooks():
253 """Reject non-callable hooks early."""
254 with pytest.raises(TypeError, match="__post_init__ must be callable"):
255 BadPostInitClass()
257 obj = BadPostSetStateClass()
258 data = pickle.dumps(obj)
259 with pytest.raises(TypeError, match="__post_setstate__ must be callable"):
260 pickle.loads(data)
262def 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)
269def test_dataclass_definition_rejection():
270 """Dataclass rejection at class-definition time."""
271 @dataclass
272 class BaseDataclass:
273 x: int
275 with pytest.raises(TypeError, match="GuardedInitMeta cannot be used with dataclass"):
276 class Child(BaseDataclass, metaclass=GuardedInitMeta):
277 pass