Skip to content

Commit 6905cf3

Browse files
committed
Use SimpleNamespace in tests; add Hypothesis
Replace tuple-based assembler fixtures with SimpleNamespace objects (providing .data, .graph, .paf_inds) and update all tests to access those attributes. Add Hypothesis to dev dependencies and introduce property-based tests for Assembly (from_array invariants and extent/area checks)
1 parent f5e6f6c commit 6905cf3

4 files changed

Lines changed: 149 additions & 127 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ tf = [
6262
dev = [
6363
"pytest",
6464
"pytest-cov",
65+
"hypothesis",
6566
"black",
6667
"ruff",
6768
]

tests/conftest.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ class FakeImageTkModule:
6767
# Assembler/assembly test fixtures
6868
# --------------------------------------------------------------------------------------
6969
@pytest.fixture
70-
def assembler_graph_and_pafs() -> tuple[list[tuple[int, int]], list[int]]:
70+
def assembler_graph_and_pafs() -> SimpleNamespace:
7171
"""Standard 2‑joint graph used throughout the test suite."""
72-
return ([(0, 1)], [0])
72+
graph = [(0, 1)]
73+
paf_inds = [0]
74+
return SimpleNamespace(graph=graph, paf_inds=paf_inds)
7375

7476

7577
@pytest.fixture
@@ -147,37 +149,37 @@ def assembler_data(
147149
assembler_graph_and_pafs,
148150
make_assembler_metadata,
149151
simple_two_label_scene,
150-
) -> tuple[dict[str, Any], list[tuple[int, int]], list[int]]:
152+
) -> SimpleNamespace:
151153
"""Full metadata + two identical frames ('0', '1')."""
152-
graph, paf_inds = assembler_graph_and_pafs
153-
data = make_assembler_metadata(graph, paf_inds, n_bodyparts=2, frame_keys=["0", "1"])
154+
paf = assembler_graph_and_pafs
155+
data = make_assembler_metadata(paf.graph, paf.paf_inds, n_bodyparts=2, frame_keys=["0", "1"])
154156
data["0"] = simple_two_label_scene
155157
data["1"] = simple_two_label_scene
156-
return data, graph, paf_inds
158+
return SimpleNamespace(data=data, graph=paf.graph, paf_inds=paf.paf_inds)
157159

158160

159161
@pytest.fixture
160162
def assembler_data_single_frame(
161163
assembler_graph_and_pafs,
162164
make_assembler_metadata,
163165
simple_two_label_scene,
164-
) -> tuple[dict[str, Any], list[tuple[int, int]], list[int]]:
166+
) -> SimpleNamespace:
165167
"""Metadata + a single frame ('0'). Used by most tests."""
166-
graph, paf_inds = assembler_graph_and_pafs
167-
data = make_assembler_metadata(graph, paf_inds, n_bodyparts=2, frame_keys=["0"])
168+
paf = assembler_graph_and_pafs
169+
data = make_assembler_metadata(paf.graph, paf.paf_inds, n_bodyparts=2, frame_keys=["0"])
168170
data["0"] = simple_two_label_scene
169-
return data, graph, paf_inds
171+
return SimpleNamespace(data=data, graph=paf.graph, paf_inds=paf.paf_inds)
170172

171173

172174
@pytest.fixture
173175
def assembler_data_two_frames_nudged(
174176
assembler_graph_and_pafs,
175177
make_assembler_metadata,
176178
simple_two_label_scene,
177-
) -> tuple[dict[str, Any], list[tuple[int, int]], list[int]]:
179+
) -> SimpleNamespace:
178180
"""Two frames where frame '1' is a nudged copy of frame '0'."""
179-
graph, paf_inds = assembler_graph_and_pafs
180-
data = make_assembler_metadata(graph, paf_inds, n_bodyparts=2, frame_keys=["0", "1"])
181+
paf = assembler_graph_and_pafs
182+
data = make_assembler_metadata(paf.graph, paf.paf_inds, n_bodyparts=2, frame_keys=["0", "1"])
181183

182184
frame0 = simple_two_label_scene
183185
frame1 = copy.deepcopy(simple_two_label_scene)
@@ -186,18 +188,18 @@ def assembler_data_two_frames_nudged(
186188

187189
data["0"] = frame0
188190
data["1"] = frame1
189-
return data, graph, paf_inds
191+
return SimpleNamespace(data=data, graph=paf.graph, paf_inds=paf.paf_inds)
190192

191193

192194
@pytest.fixture
193195
def assembler_data_no_detections(
194196
assembler_graph_and_pafs,
195197
make_assembler_metadata,
196198
make_assembler_frame,
197-
) -> tuple[dict[str, Any], list[tuple[int, int]], list[int]]:
199+
) -> SimpleNamespace:
198200
"""Metadata + a single frame ('0') with zero detections for both labels."""
199-
graph, paf_inds = assembler_graph_and_pafs
200-
data = make_assembler_metadata(graph, paf_inds, n_bodyparts=2, frame_keys=["0"])
201+
paf = assembler_graph_and_pafs
202+
data = make_assembler_metadata(paf.graph, paf.paf_inds, n_bodyparts=2, frame_keys=["0"])
201203

202204
frame = make_assembler_frame(
203205
coordinates_per_label=[np.zeros((0, 2)), np.zeros((0, 2))],
@@ -206,7 +208,8 @@ def assembler_data_no_detections(
206208
costs={},
207209
)
208210
data["0"] = frame
209-
return data, graph, paf_inds
211+
# return data, graph, paf_inds
212+
return SimpleNamespace(data=data, graph=paf.graph, paf_inds=paf.paf_inds)
210213

211214

212215
@pytest.fixture
@@ -275,16 +278,16 @@ def _factory(j1: Joint, j2: Joint, affinity: float = 1.0) -> Link:
275278
@pytest.fixture
276279
def two_overlap_assemblies(make_assembly) -> tuple[Assembly, Assembly]:
277280
"""Two assemblies with partial overlap used by intersection tests."""
278-
ass1 = make_assembly(2)
279-
ass1.data[0, :2] = [0, 0]
280-
ass1.data[1, :2] = [10, 10]
281-
ass1._visible.update({0, 1})
281+
assemb1 = make_assembly(2)
282+
assemb1.data[0, :2] = [0, 0]
283+
assemb1.data[1, :2] = [10, 10]
284+
assemb1._visible.update({0, 1})
282285

283-
ass2 = make_assembly(2)
284-
ass2.data[0, :2] = [5, 5]
285-
ass2.data[1, :2] = [15, 15]
286-
ass2._visible.update({0, 1})
287-
return ass1, ass2
286+
assemb2 = make_assembly(2)
287+
assemb2.data[0, :2] = [5, 5]
288+
assemb2.data[1, :2] = [15, 15]
289+
assemb2._visible.update({0, 1})
290+
return assemb1, assemb2
288291

289292

290293
@pytest.fixture
@@ -300,12 +303,12 @@ def soft_identity_assembly(make_assembly) -> Assembly:
300303

301304

302305
@pytest.fixture
303-
def four_joint_chain(make_joint, make_link) -> tuple[Joint, Joint, Joint, Joint, Link, Link]:
306+
def four_joint_chain(make_joint, make_link) -> SimpleNamespace:
304307
"""Four joints and two links: (0-1) and (2-3)."""
305308
j0 = make_joint((0, 0), 1.0, label=0, idx=10)
306309
j1 = make_joint((1, 0), 1.0, label=1, idx=11)
307310
j2 = make_joint((2, 0), 1.0, label=2, idx=12)
308311
j3 = make_joint((3, 0), 1.0, label=3, idx=13)
309312
l01 = make_link(j0, j1, affinity=0.5)
310313
l23 = make_link(j2, j3, affinity=0.8)
311-
return j0, j1, j2, j3, l01, l23
314+
return SimpleNamespace(j0=j0, j1=j1, j2=j2, j3=j3, l01=l01, l23=l23)

tests/tests_core/test_assembler.py

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ def _bag_from_frame(frame: dict) -> dict[int, list]:
2323

2424

2525
def test_parse_metadata_and_getitem(assembler_data, make_assembler):
26-
data, graph, paf_inds = assembler_data
26+
adat = assembler_data
2727
# Parsing
2828
asm = make_assembler(
29-
data,
29+
adat.data,
3030
max_n_individuals=2,
3131
n_multibodyparts=2,
3232
)
3333

3434
assert asm.metadata["num_joints"] == 2
35-
assert asm.metadata["paf_graph"] == graph
36-
assert list(asm.metadata["paf"]) == paf_inds
35+
assert asm.metadata["paf_graph"] == adat.graph
36+
assert list(asm.metadata["paf"]) == adat.paf_inds
3737
assert set(asm.metadata["imnames"]) == {"0", "1"}
3838
# __getitem__
3939
assert "coordinates" in asm[0]
@@ -42,13 +42,13 @@ def test_parse_metadata_and_getitem(assembler_data, make_assembler):
4242

4343

4444
def test_empty_classmethod(assembler_graph_and_pafs):
45-
graph, paf_inds = assembler_graph_and_pafs
45+
paf = assembler_graph_and_pafs
4646
empty = Assembler.empty(
4747
max_n_individuals=1,
4848
n_multibodyparts=1,
4949
n_uniquebodyparts=0,
50-
graph=graph,
51-
paf_inds=paf_inds,
50+
graph=paf.graph,
51+
paf_inds=paf.paf_inds,
5252
)
5353
assert isinstance(empty, Assembler)
5454
assert empty.n_keypoints == 1
@@ -88,14 +88,14 @@ def test_flatten_detections_with_identity(scene_copy):
8888

8989

9090
def test_extract_best_links_optimal_assignment(assembler_data_single_frame, make_assembler):
91-
data, _, _ = assembler_data_single_frame
91+
sframe_data = assembler_data_single_frame
9292
asm = make_assembler(
93-
data,
93+
sframe_data.data,
9494
greedy=False, # use Hungarian (maximize)
9595
min_n_links=1,
9696
)
9797

98-
frame0 = data["0"]
98+
frame0 = sframe_data.data["0"]
9999
bag = _bag_from_frame(frame0)
100100

101101
links = asm.extract_best_links(bag, frame0["costs"], trees=None)
@@ -111,17 +111,17 @@ def test_extract_best_links_optimal_assignment(assembler_data_single_frame, make
111111

112112

113113
def test_extract_best_links_greedy_with_thresholds(assembler_data_single_frame, make_assembler):
114-
data, _, _ = assembler_data_single_frame
114+
sframe_data = assembler_data_single_frame
115115
asm = make_assembler(
116-
data,
116+
sframe_data.data,
117117
max_n_individuals=1, # greedy will stop after 1 disjoint pair chosen
118118
greedy=True,
119119
pcutoff=0.5, # conf product must exceed 0.25
120120
min_affinity=0.5, # low-affinity pairs excluded
121121
min_n_links=1,
122122
)
123123

124-
frame0 = data["0"]
124+
frame0 = sframe_data.data["0"]
125125
bag = _bag_from_frame(frame0)
126126

127127
links = asm.extract_best_links(bag, frame0["costs"], trees=None)
@@ -140,10 +140,10 @@ def test_extract_best_links_greedy_with_thresholds(assembler_data_single_frame,
140140

141141

142142
def test_build_assemblies_from_links(assembler_data_single_frame, make_assembler):
143-
data, _, _ = assembler_data_single_frame
144-
asm = make_assembler(data, greedy=False, min_n_links=1)
143+
sframe_data = assembler_data_single_frame
144+
asm = make_assembler(sframe_data.data, greedy=False, min_n_links=1)
145145

146-
frame0 = data["0"]
146+
frame0 = sframe_data.data["0"]
147147
bag = _bag_from_frame(frame0)
148148

149149
links = asm.extract_best_links(bag, frame0["costs"])
@@ -162,26 +162,26 @@ def test_build_assemblies_from_links(assembler_data_single_frame, make_assembler
162162

163163

164164
def test__assemble_main_no_calibration_returns_two_assemblies(assembler_data_single_frame, make_assembler):
165-
data, _, _ = assembler_data_single_frame
165+
sframe_data = assembler_data_single_frame
166166
asm = make_assembler(
167-
data,
167+
sframe_data.data,
168168
greedy=False,
169169
min_n_links=1,
170170
max_overlap=0.99,
171171
window_size=0,
172172
)
173173

174-
assemblies, unique = asm._assemble(data["0"], 0)
174+
assemblies, unique = asm._assemble(sframe_data.data["0"], 0)
175175
assert unique is None
176176
assert len(assemblies) == 2
177177
assert all(len(a) == 2 for a in assemblies)
178178

179179

180180
def test__assemble_returns_none_when_no_detections(assembler_data_no_detections, make_assembler):
181-
data, _, _ = assembler_data_no_detections
182-
asm = make_assembler(data, max_n_individuals=2, n_multibodyparts=2)
181+
nodet_data = assembler_data_no_detections
182+
asm = make_assembler(nodet_data.data, max_n_individuals=2, n_multibodyparts=2)
183183

184-
assemblies, unique = asm._assemble(data["0"], 0)
184+
assemblies, unique = asm._assemble(nodet_data.data["0"], 0)
185185
assert assemblies is None and unique is None
186186

187187

@@ -191,9 +191,9 @@ def test__assemble_returns_none_when_no_detections(assembler_data_no_detections,
191191

192192

193193
def test_assemble_across_frames_updates_temporal_trees(assembler_data_two_frames_nudged, make_assembler):
194-
data, _, _ = assembler_data_two_frames_nudged
194+
twofr_data = assembler_data_two_frames_nudged
195195
asm = make_assembler(
196-
data,
196+
twofr_data.data,
197197
window_size=1, # enable temporal memory
198198
min_n_links=1,
199199
)
@@ -211,22 +211,22 @@ def test_assemble_across_frames_updates_temporal_trees(assembler_data_two_frames
211211

212212

213213
def test_identity_only_branch_groups_by_identity(assembler_data_single_frame, scene_copy, make_assembler):
214-
data, _, _ = assembler_data_single_frame
214+
sframe_data = assembler_data_single_frame
215215

216216
base = scene_copy
217217
id0 = np.array([[4.0, 1.0], [1.0, 4.0]])
218218
id1 = np.array([[4.0, 1.0], [1.0, 4.0]])
219219
base["identity"] = [id0, id1]
220-
data["0"] = base
220+
sframe_data.data["0"] = base
221221

222222
asm = make_assembler(
223-
data,
223+
sframe_data.data,
224224
max_n_individuals=3,
225225
identity_only=True,
226226
pcutoff=0.1,
227227
)
228228

229-
assemblies, _ = asm._assemble(data["0"], 0)
229+
assemblies, _ = asm._assemble(sframe_data.data["0"], 0)
230230
assert assemblies is not None
231231
assert all(len(a) >= 1 for a in assemblies)
232232

@@ -245,8 +245,8 @@ class _FakeKDE:
245245

246246

247247
def test_calc_assembly_mahalanobis_and_link_probability_with_fake_kde(assembler_data_single_frame, make_assembler):
248-
data, _, _ = assembler_data_single_frame
249-
asm = make_assembler(data, min_n_links=1)
248+
sframe_data = assembler_data_single_frame
249+
asm = make_assembler(sframe_data.data, min_n_links=1)
250250

251251
j0 = Joint((0.0, 0.0), 1.0, 0, 0)
252252
j1 = Joint((3.0, 4.0), 1.0, 1, 1)
@@ -277,11 +277,9 @@ def test_calc_assembly_mahalanobis_and_link_probability_with_fake_kde(assembler_
277277

278278

279279
def test_to_pickle_and_from_pickle(tmp_path, assembler_data_single_frame, make_assembler, assembler_graph_and_pafs):
280-
data, _, _ = assembler_data_single_frame
281-
graph, paf_inds = assembler_graph_and_pafs
282-
283-
asm = make_assembler(data, min_n_links=1)
284-
assemblies, _ = asm._assemble(data["0"], 0)
280+
sframe_data = assembler_data_single_frame
281+
asm = make_assembler(sframe_data.data, min_n_links=1)
282+
assemblies, _ = asm._assemble(sframe_data.data["0"], 0)
285283
asm.assemblies = {0: assemblies}
286284

287285
pkl = tmp_path / "assemb.pkl"
@@ -291,8 +289,8 @@ def test_to_pickle_and_from_pickle(tmp_path, assembler_data_single_frame, make_a
291289
max_n_individuals=2,
292290
n_multibodyparts=2,
293291
n_uniquebodyparts=0,
294-
graph=graph,
295-
paf_inds=paf_inds,
292+
graph=sframe_data.graph,
293+
paf_inds=sframe_data.paf_inds,
296294
)
297295
new_asm.from_pickle(str(pkl))
298296

@@ -305,10 +303,10 @@ def test_to_pickle_and_from_pickle(tmp_path, assembler_data_single_frame, make_a
305303
reason="requires PyTables",
306304
)
307305
def test_to_h5_roundtrip(tmp_path, assembler_data_single_frame, make_assembler):
308-
data, _, _ = assembler_data_single_frame
306+
sframe_data = assembler_data_single_frame
309307

310-
asm = make_assembler(data, min_n_links=1)
311-
assemblies, _ = asm._assemble(data["0"], 0)
308+
asm = make_assembler(sframe_data.data, min_n_links=1)
309+
assemblies, _ = asm._assemble(sframe_data.data["0"], 0)
312310
asm.assemblies = {0: assemblies}
313311

314312
h5 = tmp_path / "assemb.h5"

0 commit comments

Comments
 (0)