Skip to content

Commit e5bcf74

Browse files
bendichteroruebelrly
authored
add io attr (#882)
* add io attr * Add read_io as a property on AbstractContainer * Fix ruff * Fix existing tests * Added tests for AbstractContainer.read_io property * Updated changelog * Attempt to fix failing Winodws tests * Attempt to fix failing Winodws tests * Attempt to fix failing Winodws tests * Attempt to fix failing Winodws tests * Attempt to fix failing Winodws tests --------- Co-authored-by: Oliver Ruebel <[email protected]> Co-authored-by: Oliver Ruebel <[email protected]> Co-authored-by: Ryan Ly <[email protected]>
1 parent 01ab646 commit e5bcf74

File tree

4 files changed

+108
-3
lines changed

4 files changed

+108
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Added abstract static method `HDMFIO.can_read()` and concrete static method `HDF5IO.can_read()`. @bendichter [#875](https://github.com/hdmf-dev/hdmf/pull/875)
88
- Added warning for `DynamicTableRegion` links that are not added to the same parent as the original container object. @mavaylon1 [#891](https://github.com/hdmf-dev/hdmf/pull/891)
99
- Added the `TermSet` class along with integrated validation methods for any child of `AbstractContainer`, e.g., `VectorData`, `Data`, `DynamicTable`. @mavaylon1 [#880](https://github.com/hdmf-dev/hdmf/pull/880)
10+
- Added `AbstractContainer.read_io` property to be able to retrieve the HDMFIO object used for reading from the container and to ensure the I/O object used for reading is not garbage collected before the container is being deleted. @bendichter @oruebel [#882](https://github.com/hdmf-dev/hdmf/pull/882)
1011
- Allow for `datetime.date` to be used instead of `datetime.datetime`. @bendichter [#874](https://github.com/hdmf-dev/hdmf/pull/874)
1112
- Updated `HDMFIO` and `HDF5IO` to support `ExternalResources`. @mavaylon1 [#895](https://github.com/hdmf-dev/hdmf/pull/895)
1213
- Dropped Python 3.7 support. @rly [#897](https://github.com/hdmf-dev/hdmf/pull/897)

src/hdmf/backends/io.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def read(self, **kwargs):
5858
# TODO also check that the keys are appropriate. print a better error message
5959
raise UnsupportedOperation('Cannot build data. There are no values.')
6060
container = self.__manager.construct(f_builder)
61+
container.read_io = self
6162
if self.external_resources_path is not None:
6263
from hdmf.common import ExternalResources
6364
try:

src/hdmf/container.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ def __gather_fields(cls, name, bases, classdict):
192192
cls._set_fields(tuple(field_conf['name'] for field_conf in all_fields_conf))
193193
cls.__fieldsconf = tuple(all_fields_conf)
194194

195+
def __del__(self):
196+
# Make sure the reference counter for our read IO is being decremented
197+
del self.__read_io
198+
self.__read_io = None
199+
195200
def __new__(cls, *args, **kwargs):
196201
"""
197202
Static method of the object class called by Python to create the object first and then
@@ -221,6 +226,56 @@ def __init__(self, **kwargs):
221226
raise ValueError("name '" + name + "' cannot contain '/'")
222227
self.__name = name
223228
self.__field_values = dict()
229+
self.__read_io = None
230+
231+
@property
232+
def read_io(self):
233+
"""
234+
The :class:`~hdmf.backends.io.HDMFIO` object used for reading the container.
235+
236+
This property will typically be None if this Container is not a root Container
237+
(i.e., if `parent` is not None). Use `get_read_io` instead if you want to retrieve the
238+
:class:`~hdmf.backends.io.HDMFIO` object used for reading from the parent container.
239+
"""
240+
return self.__read_io
241+
242+
@read_io.setter
243+
def read_io(self, value):
244+
"""
245+
Set the io object used to read this container
246+
247+
:param value: The :class:`~hdmf.backends.io.HDMFIO` object to use
248+
:raises ValueError: If io has already been set. We can't change the IO for a container.
249+
:raises TypeError: If value is not an instance of :class:`~hdmf.backends.io.HDMFIO`
250+
"""
251+
# We do not want to import HDMFIO on the module level to avoid circular imports. Since we only need
252+
# it for type checking we import it here.
253+
from hdmf.backends.io import HDMFIO
254+
if not isinstance(value, HDMFIO):
255+
raise TypeError("io must be an instance of HDMFIO")
256+
if self.__read_io is not None:
257+
raise ValueError("io has already been set for this container (name=%s, type=%s)" %
258+
(self.name, str(type(self))))
259+
else:
260+
self.__read_io = value
261+
262+
def get_read_io(self):
263+
"""
264+
Get the io object used to read this container.
265+
266+
If `self.read_io` is None, this function will iterate through the parents and return the
267+
first `io` object found on a parent container
268+
269+
:returns: The :class:`~hdmf.backends.io.HDMFIO` object used to read this container.
270+
Returns None in case no io object is found, e.g., in case this container has
271+
not been read from file.
272+
"""
273+
curr_obj = self
274+
re_io = self.read_io
275+
while re_io is None and curr_obj.parent is not None:
276+
curr_obj = curr_obj.parent
277+
re_io = curr_obj.read_io
278+
return re_io
224279

225280
@property
226281
def name(self):

tests/unit/test_container.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
22
from uuid import uuid4, UUID
3+
import os
34

45
from hdmf.container import AbstractContainer, Container, Data, ExternalResourcesManager
56
from hdmf.common.resources import ExternalResources
@@ -8,6 +9,7 @@
89
from hdmf.common import (DynamicTable, VectorData, DynamicTableRegion)
910
import unittest
1011
from hdmf.term_set import TermSet
12+
from hdmf.backends.hdf5.h5tools import HDF5IO
1113

1214
try:
1315
import linkml_runtime # noqa: F401
@@ -41,6 +43,13 @@ def test_link_and_get_resources(self):
4143

4244
class TestContainer(TestCase):
4345

46+
def setUp(self):
47+
self.path = "test_container.h5"
48+
49+
def tearDown(self):
50+
if os.path.exists(self.path):
51+
os.remove(self.path)
52+
4453
def test_new(self):
4554
"""Test that __new__ properly sets parent and other fields.
4655
"""
@@ -82,6 +91,45 @@ def test_init(self):
8291
self.assertEqual(obj.children, tuple())
8392
self.assertIsNone(obj.parent)
8493
self.assertEqual(obj.name, 'obj1')
94+
self.assertIsNone(obj.read_io)
95+
96+
def test_read_io_none(self):
97+
"""Test that __init__ properly sets read_io to None"""
98+
obj = Container('obj1')
99+
self.assertIsNone(obj.read_io)
100+
101+
def test_read_io_setter(self):
102+
"""Test setting the read IO property"""
103+
obj = Container('obj1')
104+
# Bad value for read_io
105+
with self.assertRaises(TypeError):
106+
obj.read_io = "test"
107+
# Set read_io
108+
with HDF5IO(self.path, mode='w') as temp_io:
109+
obj.read_io = temp_io
110+
self.assertIs(obj.read_io, temp_io)
111+
# Check that setting read_io again fails
112+
with self.assertRaises(ValueError):
113+
obj.read_io = temp_io
114+
115+
def test_get_read_io_on_self(self):
116+
"""Test that get_read_io works when the container is set on the container"""
117+
obj = Container('obj1')
118+
self.assertIsNone(obj.get_read_io())
119+
with HDF5IO(self.path, mode='w') as temp_io:
120+
obj.read_io = temp_io
121+
re_io = obj.get_read_io()
122+
self.assertIs(re_io, temp_io)
123+
124+
def test_get_read_io_on_parent(self):
125+
"""Test that get_read_io works when the container is set on the parent"""
126+
parent_obj = Container('obj1')
127+
child_obj = Container('obj2')
128+
child_obj.parent = parent_obj
129+
with HDF5IO(self.path, mode='w') as temp_io:
130+
parent_obj.read_io = temp_io
131+
self.assertIsNone(child_obj.read_io)
132+
self.assertIs(child_obj.get_read_io(), temp_io)
85133

86134
def test_set_parent(self):
87135
"""Test that parent setter properly sets parent
@@ -481,7 +529,7 @@ class EmptyFields(AbstractContainer):
481529
self.assertTupleEqual(EmptyFields.get_fields_conf(), tuple())
482530

483531
props = TestAbstractContainerFieldsConf.find_all_properties(EmptyFields)
484-
expected = ['children', 'container_source', 'fields', 'modified', 'name', 'object_id', 'parent']
532+
expected = ['children', 'container_source', 'fields', 'modified', 'name', 'object_id', 'parent', 'read_io']
485533
self.assertListEqual(props, expected)
486534

487535
def test_named_fields(self):
@@ -502,7 +550,7 @@ def __init__(self, **kwargs):
502550

503551
props = TestAbstractContainerFieldsConf.find_all_properties(NamedFields)
504552
expected = ['children', 'container_source', 'field1', 'field2', 'fields', 'modified', 'name', 'object_id',
505-
'parent']
553+
'parent', 'read_io']
506554
self.assertListEqual(props, expected)
507555

508556
f1_doc = getattr(NamedFields, 'field1').__doc__
@@ -583,7 +631,7 @@ class NamedFieldsChild(NamedFields):
583631

584632
props = TestAbstractContainerFieldsConf.find_all_properties(NamedFieldsChild)
585633
expected = ['children', 'container_source', 'field1', 'field2', 'fields', 'modified', 'name', 'object_id',
586-
'parent']
634+
'parent', 'read_io']
587635
self.assertListEqual(props, expected)
588636

589637
def test_inheritance_override(self):

0 commit comments

Comments
 (0)