Skip to content

Commit be52652

Browse files
rlymavaylon1
andauthored
Refactor DTR warning (#917)
* Refactor DTR warning * Fix --------- Co-authored-by: Matthew Avaylon <[email protected]>
1 parent dd39b38 commit be52652

File tree

5 files changed

+106
-43
lines changed

5 files changed

+106
-43
lines changed

src/hdmf/common/table.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,26 @@ def __repr__(self):
14211421
id(self.table))
14221422
return template
14231423

1424+
def _validate_on_set_parent(self):
1425+
# when this DynamicTableRegion is added to a parent, check:
1426+
# 1) if the table was read from a written file, no need to validate further
1427+
p = self.table
1428+
while p is not None:
1429+
if p.container_source is not None:
1430+
return super()._validate_on_set_parent()
1431+
p = p.parent
1432+
1433+
# 2) if none of the ancestors are ancestors of the linked-to table, then when this is written, the table
1434+
# field will point to a table that is not in the file
1435+
table_ancestor_ids = [id(x) for x in self.table.get_ancestors()]
1436+
self_ancestor_ids = [id(x) for x in self.get_ancestors()]
1437+
1438+
if set(table_ancestor_ids).isdisjoint(self_ancestor_ids):
1439+
msg = (f"The linked table for DynamicTableRegion '{self.name}' does not share an ancestor with the "
1440+
"DynamicTableRegion.")
1441+
warn(msg)
1442+
return super()._validate_on_set_parent()
1443+
14241444

14251445
def _uint_precision(elements):
14261446
""" Calculate the uint precision needed to encode a set of elements """

src/hdmf/container.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ def get_ancestor(self, **kwargs):
302302
p = p.parent
303303
return None
304304

305+
@docval()
306+
def get_ancestors(self, **kwargs):
307+
p = self.parent
308+
ret = []
309+
while p is not None:
310+
ret.append(p)
311+
p = p.parent
312+
return tuple(ret)
313+
305314
@property
306315
def fields(self):
307316
'''
@@ -414,12 +423,8 @@ def parent(self, parent_container):
414423
parent_container.__children.append(self)
415424
parent_container.set_modified()
416425
for child in self.children:
417-
if type(child).__name__ == "DynamicTableRegion":
418-
if child.table.parent is None:
419-
msg = "The table for this DynamicTableRegion has not been added to the parent."
420-
warn(msg)
421-
else:
422-
continue
426+
# used by hdmf.common.table.DynamicTableRegion to check for orphaned tables
427+
child._validate_on_set_parent()
423428

424429
def _remove_child(self, child):
425430
"""Remove a child Container. Intended for use in subclasses that allow dynamic addition of child Containers."""
@@ -445,6 +450,14 @@ def reset_parent(self):
445450
else:
446451
raise ValueError("Cannot reset parent when parent is not an AbstractContainer: %s" % repr(self.parent))
447452

453+
def _validate_on_set_parent(self):
454+
"""Validate this Container after setting the parent.
455+
456+
This method is called by the parent setter. It can be overridden in subclasses to perform additional
457+
validation. The default implementation does nothing.
458+
"""
459+
pass
460+
448461

449462
class Container(AbstractContainer):
450463
"""A container that can contain other containers and has special functionality for printing."""

tests/unit/common/test_linkedtables.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Module for testing functions specific to tables containing DynamicTableRegion columns
33
"""
44

5+
import warnings
56
import numpy as np
67
from hdmf.common import DynamicTable, AlignedDynamicTable, VectorData, DynamicTableRegion, VectorIndex
78
from hdmf.testing import TestCase
@@ -139,11 +140,16 @@ def setUp(self):
139140
description='filter value',
140141
index=False)
141142
# Aligned table
142-
self.aligned_table = AlignedDynamicTable(name='my_aligned_table',
143-
description='my test table',
144-
columns=[VectorData(name='a1', description='a1', data=np.arange(3)), ],
145-
colnames=['a1', ],
146-
category_tables=[self.category0, self.category1])
143+
with warnings.catch_warnings():
144+
msg = "The linked table for DynamicTableRegion '.*' does not share an ancestor with the DynamicTableRegion."
145+
warnings.filterwarnings("ignore", category=UserWarning, message=msg)
146+
self.aligned_table = AlignedDynamicTable(
147+
name='my_aligned_table',
148+
description='my test table',
149+
columns=[VectorData(name='a1', description='a1', data=np.arange(3)), ],
150+
colnames=['a1', ],
151+
category_tables=[self.category0, self.category1]
152+
)
147153

148154
def tearDown(self):
149155
del self.table_level0_0
@@ -241,13 +247,16 @@ def test_get_foreign_column_in_main_and_category_table(self):
241247
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
242248
DynamicTableRegion(name='c2', description='c2',
243249
data=np.arange(4), table=temp_table0)])
244-
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
245-
description='my test table',
246-
category_tables=[temp_table],
247-
colnames=['a1', 'a2'],
248-
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
249-
DynamicTableRegion(name='a2', description='c2',
250-
data=np.arange(4), table=temp_table)])
250+
with warnings.catch_warnings():
251+
msg = "The linked table for DynamicTableRegion '.*' does not share an ancestor with the DynamicTableRegion."
252+
warnings.filterwarnings("ignore", category=UserWarning, message=msg)
253+
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
254+
description='my test table',
255+
category_tables=[temp_table],
256+
colnames=['a1', 'a2'],
257+
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
258+
DynamicTableRegion(name='a2', description='c2',
259+
data=np.arange(4), table=temp_table)])
251260
# We should get both the DynamicTableRegion from the main table and the category 't1'
252261
self.assertListEqual(temp_aligned_table.get_foreign_columns(), [(None, 'a2'), ('t1', 'c2')])
253262
# We should only get the column from the main table
@@ -275,12 +284,15 @@ def test_get_linked_tables_none(self):
275284
colnames=['c1', 'c2'],
276285
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
277286
VectorData(name='c2', description='c2', data=np.arange(4))])
278-
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
279-
description='my test table',
280-
category_tables=[temp_table],
281-
colnames=['a1', 'a2'],
282-
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
283-
VectorData(name='a2', description='c2', data=np.arange(4))])
287+
with warnings.catch_warnings():
288+
msg = "The linked table for DynamicTableRegion '.*' does not share an ancestor with the DynamicTableRegion."
289+
warnings.filterwarnings("ignore", category=UserWarning, message=msg)
290+
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
291+
description='my test table',
292+
category_tables=[temp_table],
293+
colnames=['a1', 'a2'],
294+
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
295+
VectorData(name='a2', description='c2', data=np.arange(4))])
284296
self.assertListEqual(temp_aligned_table.get_linked_tables(), [])
285297
self.assertListEqual(temp_aligned_table.get_linked_tables(ignore_category_tables=True), [])
286298

@@ -294,13 +306,16 @@ def test_get_linked_tables_complex_link(self):
294306
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
295307
DynamicTableRegion(name='c2', description='c2',
296308
data=np.arange(4), table=temp_table0)])
297-
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
298-
description='my test table',
299-
category_tables=[temp_table],
300-
colnames=['a1', 'a2'],
301-
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
302-
DynamicTableRegion(name='a2', description='c2',
303-
data=np.arange(4), table=temp_table)])
309+
with warnings.catch_warnings():
310+
msg = "The linked table for DynamicTableRegion '.*' does not share an ancestor with the DynamicTableRegion."
311+
warnings.filterwarnings("ignore", category=UserWarning, message=msg)
312+
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
313+
description='my test table',
314+
category_tables=[temp_table],
315+
colnames=['a1', 'a2'],
316+
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
317+
DynamicTableRegion(name='a2', description='c2',
318+
data=np.arange(4), table=temp_table)])
304319
# NOTE: in this example templ_aligned_table both points to temp_table and at the
305320
# same time contains temp_table as a category. This could lead to temp_table
306321
# visited multiple times and we want to make sure this doesn't happen
@@ -326,17 +341,20 @@ def test_get_linked_tables_simple_link(self):
326341
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
327342
VectorData(name='c2', description='c2', data=np.arange(4))])
328343
temp_table = DynamicTable(name='t1', description='t1',
329-
colnames=['c1', 'c2'],
330-
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
331-
DynamicTableRegion(name='c2', description='c2',
332-
data=np.arange(4), table=temp_table0)])
333-
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
334-
description='my test table',
335-
category_tables=[temp_table],
336-
colnames=['a1', 'a2'],
337-
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
338-
DynamicTableRegion(name='a2', description='c2',
339-
data=np.arange(4), table=temp_table0)])
344+
colnames=['c1', 'c2'],
345+
columns=[VectorData(name='c1', description='c1', data=np.arange(4)),
346+
DynamicTableRegion(name='c2', description='c2',
347+
data=np.arange(4), table=temp_table0)])
348+
with warnings.catch_warnings():
349+
msg = "The linked table for DynamicTableRegion '.*' does not share an ancestor with the DynamicTableRegion."
350+
warnings.filterwarnings("ignore", category=UserWarning, message=msg)
351+
temp_aligned_table = AlignedDynamicTable(name='my_aligned_table',
352+
description='my test table',
353+
category_tables=[temp_table],
354+
colnames=['a1', 'a2'],
355+
columns=[VectorData(name='a1', description='c1', data=np.arange(4)),
356+
DynamicTableRegion(name='a2', description='c2',
357+
data=np.arange(4), table=temp_table0)])
340358
# NOTE: in this example temp_aligned_table and temp_table both point to temp_table0
341359
# We should get both the DynamicTableRegion from the main table and the category 't1'
342360
linked_tables = temp_aligned_table.get_linked_tables()

tests/unit/common/test_table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ def setUp(self):
11241124
super().setUp()
11251125

11261126
def setUpContainer(self):
1127-
multi_container = SimpleMultiContainer(name='multi', containers=[self.table, self.target_table])
1127+
multi_container = SimpleMultiContainer(name='multi', containers=[self.target_table, self.table])
11281128
return multi_container
11291129

11301130
def _get(self, arg):

tests/unit/test_container.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,18 @@ def test_reset_parent_no_parent(self):
382382
obj.reset_parent()
383383
self.assertIsNone(obj.parent)
384384

385+
def test_get_ancestors(self):
386+
"""Test that get_ancestors returns the correct ancestors.
387+
"""
388+
grandparent_obj = Container('obj1')
389+
parent_obj = Container('obj2')
390+
child_obj = Container('obj3')
391+
parent_obj.parent = grandparent_obj
392+
child_obj.parent = parent_obj
393+
self.assertTupleEqual(grandparent_obj.get_ancestors(), tuple())
394+
self.assertTupleEqual(parent_obj.get_ancestors(), (grandparent_obj, ))
395+
self.assertTupleEqual(child_obj.get_ancestors(), (parent_obj, grandparent_obj))
396+
385397

386398
class TestHTMLRepr(TestCase):
387399

0 commit comments

Comments
 (0)